Compare commits

...

106 Commits

Author SHA1 Message Date
Jay Lorch
5112ccf492 Verus verification 2026-04-06 11:31:41 -07:00
Anand Krishnamoorthi
8f740e2f6f feat(azure-policy): add policy rule and policy definition parsers (#660)
Extend the Azure Policy parser to handle complete policyRule and
policyDefinition JSON structures, not just standalone constraints.

Policy rule parser (policy_rule.rs):
- Parse top-level { "if": ..., "then": ... } objects
- Extract effect kind (deny, audit, append, modify, etc.) into typed AST
- Parse "details" structurally when it is an object to pull out
  existenceCondition as a first-class Constraint; fall back to opaque
  JSON for non-object details (e.g. append array form)
- Detect duplicate/missing keys for "if", "then", "effect", "details"

Policy definition parser (policy_definition.rs):
- Handle both wrapped ARM envelope ({ "properties": { ... } }) and
  unwrapped (properties-level keys at top level) forms
- Type-extract displayName, description, mode, metadata, parameters,
  and policyRule; everything else goes into extra
- Parse parameter definitions with type, defaultValue, allowedValues,
  and metadata; detect duplicate parameter names
- Duplicate key detection throughout

Grammar documentation (docs/azure-policy/azurepolicy.ebnf):
- Add formal EBNF grammar covering policy-rule, then-block,
  constraints, conditions, all 19 operators, count expressions,
  JSON values, and ARM template expressions

Test harness changes:
- Add parse_level field to YAML test cases: "constraint" (default),
  "policy_rule", or "policy_definition"
- Un-skip three parse_errors cases that needed policy_rule-level parsing
- Add policy_rule.yaml with 12 cases covering all 9 effect kinds,
  existenceCondition, parameterized effects, complex conditions, and
  extra key handling
- Add policy_definition.yaml with wrapped, unwrapped, parameterized,
  missing-policyRule, and duplicate-key error cases
2026-04-06 11:36:24 -05:00
Anand Krishnamoorthi
687be2850b feat: add Azure Policy constraint parser (#658)
Add constraint.rs module that parses Azure Policy JSON constraints
into span-annotated AST nodes:

- Logical combinators: allOf, anyOf, not
- Leaf conditions: field/value with all 19 operators
- Count blocks: field-count and value-count with where clauses

Public API: parse_constraint() parses a standalone constraint from JSON.

Includes YAML-driven test suite with 6 test files covering operators,
fields, expressions, logical combinators, count, and parse errors.
2026-04-03 19:09:51 -05:00
Anand Krishnamoorthi
95bffcb5f9 feat(rvm): extend program metadata and bump serialization to v6 (#654)
Add typed metadata support to RVM programs so that language frontends
can store language identity and arbitrary annotations alongside the
compiled bytecode.

Program metadata:
- Add `language` field to identify the source language (e.g. "rego",
  "azure_policy") so the VM can adjust semantics at runtime
- Add `annotations` map (BTreeMap<String, MetadataValue>) for
  frontend-specific key-value metadata
- Add MetadataValue enum with String, Bool, Integer, Float, Array,
  and Object variants, plus full serde support
- Add to_value() conversion for runtime access from VM instructions
- Add has_host_await flag with recompute_host_await_presence()

Serialization:
- Bump binary format version from 5 to 6
- Add JSON serialization for the new metadata fields

Assembly listing:
- Display language and annotations in the program header

Compiler:
- Track has_host_await during Rego compilation
2026-04-03 12:53:42 -05:00
dependabot[bot]
db8a9abf13 build(deps): bump minitest in /bindings/ruby in the per-dependency group (#656)
Bumps the per-dependency group in /bindings/ruby with 1 update: [minitest](https://github.com/minitest/minitest).


Updates `minitest` from 6.0.2 to 6.0.3
- [Changelog](https://github.com/minitest/minitest/blob/master/History.rdoc)
- [Commits](https://github.com/minitest/minitest/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: minitest
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 13:08:40 -05:00
dependabot[bot]
421ee6af9b build(deps): bump the rust-dependencies group across 2 directories with 3 updates (#657)
Bumps the rust-dependencies group with 2 updates in the / directory: [toml_edit](https://github.com/toml-rs/toml) and [zip](https://github.com/zip-rs/zip2).
Bumps the rust-dependencies group with 1 update in the /bindings/wasm directory: [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen).


Updates `toml_edit` from 0.25.8+spec-1.1.0 to 0.25.10+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.25.8...v0.25.10)

Updates `zip` from 8.4.0 to 8.5.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v8.4.0...v8.5.0)

Updates `wasm-bindgen-test` from 0.3.66 to 0.3.67
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: toml_edit
  dependency-version: 0.25.10+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 8.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wasm-bindgen-test
  dependency-version: 0.3.67
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 13:08:12 -05:00
Anand Krishnamoorthi
64f71dee34 feat: add Azure Policy core JSON parser and expression parser (#655)
Add the foundational parsing infrastructure for Azure Policy JSON:

- ExprParser: ARM template expression parser for "[...]" strings,
  supporting function calls, dot access, index access, and literals
- Parser (core): recursive-descent JSON tokenizer-to-AST parser that
  reads directly from Lexer tokens with no intermediate serde_json step
- ParseError: structured error types with span context for diagnostics
- Helper functions: field classification, operator kind parsing, and
  ARM template expression detection

These components are consumed by the policy-aware parsing modules
(constraint, policy_rule, policy_definition) in a subsequent PR.
2026-04-02 11:42:03 -05:00
Anand Krishnamoorthi
648ba40126 feat: add Azure Policy AST types (#653)
Add span-annotated AST types for Azure Policy conditions and rules.

- PolicyDefinition, PolicyRule with if/then/details structure
- Condition enum: field conditions, value conditions, logical
  combinators (allOf, anyOf, not), and count expressions
- Operator enums for all 19 Azure Policy constraint operators
  (equals, contains, greater, matchInsensitively, etc.)
- Expr enum for field references, literal values (number, string,
  bool), template function calls, and policy function invocations
- Value types with span tracking for error reporting
2026-04-01 11:54:28 -05:00
Anand Krishnamoorthi
126cc12eb5 refactor: consolidate RVM instruction variants and clean up VM internals (#651)
Merge the three separate Assert* instructions (AssertNot, AssertCondition,
AssertNotUndefined) into a single `Guard { register, mode }` instruction
with a GuardMode enum. This cuts duplicated match arms across display,
listing, parser, dispatch, and all compiler emit sites.

Drop the unnecessary `#[repr(C)]` from the Instruction enum. It was never
exposed across FFI, so the C-compatible 4-byte discriminant was pure waste.
Without it Rust picks a 1-byte discriminant, shrinking every instruction
from 8 bytes to 6. A new `instruction_size` unit test locks this at 6.

While touching these files, also clean up several long-standing issues:

- Deduplicate the iteration-state setup in loops.rs by extracting a shared
  resolve_iteration_state() helper -- the stack-based and stackless paths
  had near-identical 40-line blocks.
- Collapse the ExitWithSuccess / ExitWithFailure match arms into one.
- In rules.rs, stop cloning Arc<Program> just to borrow a RuleInfo -- clone
  the small RuleInfo struct directly and extract a get_rule_info() helper.
- Move the memory check into dispatch (runs per instruction) and remove the
  now-dead enforce_memory_check() entry-point calls.
- Apply map_or_else style throughout listing.rs for consistency.
2026-04-01 05:34:33 -05:00
dependabot[bot]
1a8fc08773 build(deps): bump wasm-bindgen-test (#650)
Bumps the rust-dependencies group with 1 update in the /bindings/wasm directory: [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen).


Updates `wasm-bindgen-test` from 0.3.65 to 0.3.66
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: wasm-bindgen-test
  dependency-version: 0.3.66
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:56:14 -05:00
dependabot[bot]
c164917d63 build(deps): bump rb_sys in /bindings/ruby in the per-dependency group (#649)
Bumps the per-dependency group in /bindings/ruby with 1 update: [rb_sys](https://github.com/oxidize-rb/rb-sys).


Updates `rb_sys` from 0.9.124 to 0.9.125
- [Release notes](https://github.com/oxidize-rb/rb-sys/releases)
- [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.124...v0.9.125)

---
updated-dependencies:
- dependency-name: rb_sys
  dependency-version: 0.9.125
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:55:45 -05:00
dependabot[bot]
6a6cc659b7 build(deps): bump the rust-dependencies group across 3 directories with 4 updates (#647)
Bumps the rust-dependencies group with 2 updates in the / directory: [toml_edit](https://github.com/toml-rs/toml) and [zip](https://github.com/zip-rs/zip2).
Bumps the rust-dependencies group with 1 update in the /bindings/python directory: [ordered-float](https://github.com/reem/rust-ordered-float).
Bumps the rust-dependencies group with 1 update in the /bindings/wasm directory: [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen).


Updates `toml_edit` from 0.22.27 to 0.25.8+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.22.27...v0.25.8)

Updates `zip` from 0.6.6 to 8.4.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/commits/v8.4.0)

Updates `ordered-float` from 5.2.0 to 5.3.0
- [Release notes](https://github.com/reem/rust-ordered-float/releases)
- [Commits](https://github.com/reem/rust-ordered-float/compare/v5.2.0...v5.3.0)

Updates `wasm-bindgen-test` from 0.3.64 to 0.3.65
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: toml_edit
  dependency-version: 0.25.8+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 8.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: ordered-float
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wasm-bindgen-test
  dependency-version: 0.3.65
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 07:16:10 -05:00
dependabot[bot]
a86cf1119f ci(deps): bump the github-actions group across 1 directory with 3 updates (#646)
Bumps the github-actions group with 3 updates in the / directory: [actions/setup-go](https://github.com/actions/setup-go), [github/codeql-action](https://github.com/github/codeql-action) and [ruby/setup-ruby](https://github.com/ruby/setup-ruby).


Updates `actions/setup-go` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4b73464bb3...4a3601121d)

Updates `github/codeql-action` from 4.34.1 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](3869755554...c10b8064de)

Updates `ruby/setup-ruby` from 1.295.0 to 1.299.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](319994f95f...3ff19f5e2b)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: ruby/setup-ruby
  dependency-version: 1.299.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 07:15:45 -05:00
Anand Krishnamoorthi
989ca6df2e ci(dependabot): restore cargo dependency grouping (#645)
Without grouping, dependabot creates a separate PR per directory for the
same dependency. Each individual PR fails to build due to version skew
across the root workspace and binding crates.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-30 18:44:53 -05:00
Anand Krishnamoorthi
d36f952133 feat(azure-policy): add alias normalization and denormalization (#635)
* feat: add Azure Policy alias normalization/denormalization

Add normalizer and denormalizer for ARM JSON resources, enabling Azure
Policy alias short names to become direct paths into a flat structure.

- Normalizer: flattens properties wrappers, lowercases keys, resolves
  per-alias versioned ARM paths, handles sub-resource array flattening,
  element-level field remaps, and array base renames
- Denormalizer: reverses all transformations with casing restoration
- AliasRegistry: loads production alias catalogs and data policy manifests
- Types: serde deserialization for ARM provider alias formats
- YAML test suite: 13 test files covering normalize, denormalize, round-trip,
  data-plane, edge cases, malformed input, sub-resources, and registry API
- Benchmark suite for normalization performance

* feat: add FFI and C# bindings for alias normalization

- FFI: alias_registry.rs with C-compatible API for loading catalogs,
  normalizing resources, and denormalizing back to ARM JSON
- C#: AliasRegistry wrapper class with NativeMethods P/Invoke bindings
  and integration tests
- Updated Cargo.lock files for new serde_json dependency
2026-03-30 18:44:36 -05:00
Anand Krishnamoorthi
35fb5d5953 Fix build break (#634)
* fix: update bindings and builtins for breaking dependency upgrades

- Update rand 0.10 API: use RngExt trait instead of removed Rng trait
- Update jsonschema 0.45 API: replace removed BasicOutput/apply with
  iter_errors for schema validation
- Update jni 0.22 API: migrate from deprecated JNIEnv to EnvUnowned
  with_env pattern, replace deprecated get_string/new_string/throw
  methods with their modern equivalents
- Update pyo3 0.28 API: replace removed PyObject with Py<PyAny>,
  deprecated downcast with cast, and removed with_gil with attach

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci(dependabot): create per-dependency PRs for Cargo updates

Remove the groups.rust-dependencies catch-all group so Dependabot
opens a separate PR for each Cargo dependency update instead of
bundling them all into a single PR.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: enable getrandom wasm_js feature for wasm32-unknown-unknown builds

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address PR review comments

- Stream iter_errors directly into BTreeSet without intermediate Vec
- Use JNI_TRUE/JNI_FALSE for jboolean instead of bool coercion

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-27 12:34:49 -05:00
dependabot[bot]
296b34171a build(deps): bump the rust-dependencies group across 5 directories with 16 updates (#633)
* build(deps): bump the rust-dependencies group across 5 directories with 16 updates

Bumps the rust-dependencies group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.102` |
| [spin](https://github.com/mvdnes/spin-rs) | `0.9.8` | `0.10.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.20.0` | `1.22.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.30.0` | `0.45.0` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.43` | `0.4.44` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.11.0` | `2.12.0` |
| [rand](https://github.com/rust-random/rand) | `0.9.2` | `0.10.0` |
| [clap](https://github.com/clap-rs/clap) | `4.5.56` | `4.5.60` |
| [criterion](https://github.com/criterion-rs/criterion.rs) | `0.8.1` | `0.8.2` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.22.27` | `0.25.8+spec-1.1.0` |
| [zip](https://github.com/zip-rs/zip2) | `0.6.6` | `8.4.0` |

Bumps the rust-dependencies group with 9 updates in the /bindings/ffi directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.102` |
| [spin](https://github.com/mvdnes/spin-rs) | `0.9.8` | `0.10.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.20.0` | `1.22.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.30.0` | `0.45.0` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.43` | `0.4.44` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.11.0` | `2.12.0` |
| [rand](https://github.com/rust-random/rand) | `0.9.2` | `0.10.0` |
| [clap](https://github.com/clap-rs/clap) | `4.5.56` | `4.6.0` |

Bumps the rust-dependencies group with 9 updates in the /bindings/java directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.102` |
| [spin](https://github.com/mvdnes/spin-rs) | `0.9.8` | `0.10.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.20.0` | `1.22.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.30.0` | `0.45.0` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.43` | `0.4.44` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.11.0` | `2.12.0` |
| [rand](https://github.com/rust-random/rand) | `0.9.2` | `0.10.0` |
| [jni](https://github.com/jni-rs/jni-rs) | `0.21.1` | `0.22.4` |

Bumps the rust-dependencies group with 10 updates in the /bindings/python directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.102` |
| [spin](https://github.com/mvdnes/spin-rs) | `0.9.8` | `0.10.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.20.0` | `1.22.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.30.0` | `0.45.0` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.43` | `0.4.44` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.11.0` | `2.12.0` |
| [rand](https://github.com/rust-random/rand) | `0.9.2` | `0.10.0` |
| [ordered-float](https://github.com/reem/rust-ordered-float) | `5.1.0` | `5.2.0` |
| [pyo3](https://github.com/pyo3/pyo3) | `0.24.2` | `0.28.2` |

Bumps the rust-dependencies group with 9 updates in the /bindings/wasm directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.100` | `1.0.102` |
| [spin](https://github.com/mvdnes/spin-rs) | `0.9.8` | `0.10.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.2` | `1.12.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.20.0` | `1.22.0` |
| [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.30.0` | `0.45.0` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.43` | `0.4.44` |
| [ipnet](https://github.com/krisprice/ipnet) | `2.11.0` | `2.12.0` |
| [rand](https://github.com/rust-random/rand) | `0.9.2` | `0.10.0` |
| [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen) | `0.3.58` | `0.3.64` |



Updates `anyhow` from 1.0.100 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.102)

Updates `spin` from 0.9.8 to 0.10.0
- [Changelog](https://github.com/zesterer/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `uuid` from 1.20.0 to 1.22.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.20.0...v1.22.0)

Updates `jsonschema` from 0.30.0 to 0.45.0
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/rust-v0.30.0...ruby-v0.45.0)

Updates `chrono` from 0.4.43 to 0.4.44
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

Updates `ipnet` from 2.11.0 to 2.12.0
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/compare/2.11.0...2.12.0)

Updates `rand` from 0.9.2 to 0.10.0
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

Updates `clap` from 4.5.56 to 4.5.60
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.56...clap_complete-v4.5.60)

Updates `criterion` from 0.8.1 to 0.8.2
- [Release notes](https://github.com/criterion-rs/criterion.rs/releases)
- [Changelog](https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.8.1...criterion-v0.8.2)

Updates `toml_edit` from 0.22.27 to 0.25.8+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.22.27...v0.25.8)

Updates `zip` from 0.6.6 to 8.4.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/commits/v8.4.0)

Updates `anyhow` from 1.0.100 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.102)

Updates `spin` from 0.9.8 to 0.10.0
- [Changelog](https://github.com/zesterer/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `uuid` from 1.20.0 to 1.22.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.20.0...v1.22.0)

Updates `jsonschema` from 0.30.0 to 0.45.0
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/rust-v0.30.0...ruby-v0.45.0)

Updates `chrono` from 0.4.43 to 0.4.44
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

Updates `ipnet` from 2.11.0 to 2.12.0
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/compare/2.11.0...2.12.0)

Updates `rand` from 0.9.2 to 0.10.0
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

Updates `clap` from 4.5.56 to 4.6.0
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.56...clap_complete-v4.5.60)

Updates `anyhow` from 1.0.100 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.102)

Updates `spin` from 0.9.8 to 0.10.0
- [Changelog](https://github.com/zesterer/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `uuid` from 1.20.0 to 1.22.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.20.0...v1.22.0)

Updates `jsonschema` from 0.30.0 to 0.45.0
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/rust-v0.30.0...ruby-v0.45.0)

Updates `chrono` from 0.4.43 to 0.4.44
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

Updates `ipnet` from 2.11.0 to 2.12.0
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/compare/2.11.0...2.12.0)

Updates `rand` from 0.9.2 to 0.10.0
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

Updates `jni` from 0.21.1 to 0.22.4
- [Release notes](https://github.com/jni-rs/jni-rs/releases)
- [Changelog](https://github.com/jni-rs/jni-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jni-rs/jni-rs/compare/v0.21.1...v0.22.4)

Updates `anyhow` from 1.0.100 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.102)

Updates `spin` from 0.9.8 to 0.10.0
- [Changelog](https://github.com/zesterer/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `uuid` from 1.20.0 to 1.22.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.20.0...v1.22.0)

Updates `jsonschema` from 0.30.0 to 0.45.0
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/rust-v0.30.0...ruby-v0.45.0)

Updates `chrono` from 0.4.43 to 0.4.44
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

Updates `ipnet` from 2.11.0 to 2.12.0
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/compare/2.11.0...2.12.0)

Updates `rand` from 0.9.2 to 0.10.0
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

Updates `ordered-float` from 5.1.0 to 5.2.0
- [Release notes](https://github.com/reem/rust-ordered-float/releases)
- [Commits](https://github.com/reem/rust-ordered-float/compare/v5.1.0...v5.2.0)

Updates `pyo3` from 0.24.2 to 0.28.2
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.24.2...v0.28.2)

Updates `anyhow` from 1.0.100 to 1.0.102
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.100...1.0.102)

Updates `spin` from 0.9.8 to 0.10.0
- [Changelog](https://github.com/zesterer/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

Updates `regex` from 1.12.2 to 1.12.3
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.2...1.12.3)

Updates `uuid` from 1.20.0 to 1.22.0
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.20.0...v1.22.0)

Updates `jsonschema` from 0.30.0 to 0.45.0
- [Release notes](https://github.com/Stranger6667/jsonschema/releases)
- [Changelog](https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/jsonschema/compare/rust-v0.30.0...ruby-v0.45.0)

Updates `chrono` from 0.4.43 to 0.4.44
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.43...v0.4.44)

Updates `ipnet` from 2.11.0 to 2.12.0
- [Release notes](https://github.com/krisprice/ipnet/releases)
- [Changelog](https://github.com/krisprice/ipnet/blob/master/RELEASES.md)
- [Commits](https://github.com/krisprice/ipnet/compare/2.11.0...2.12.0)

Updates `rand` from 0.9.2 to 0.10.0
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/rand_core-0.9.2...0.10.0)

Updates `wasm-bindgen-test` from 0.3.58 to 0.3.64
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jsonschema
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ipnet
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.5.60
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: criterion
  dependency-version: 0.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.25.8+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 8.4.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jsonschema
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ipnet
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: clap
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jsonschema
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ipnet
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jni
  dependency-version: 0.22.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jsonschema
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ipnet
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: ordered-float
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: pyo3
  dependency-version: 0.28.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-version: 1.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-version: 1.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: uuid
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: jsonschema
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ipnet
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rand
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wasm-bindgen-test
  dependency-version: 0.3.64
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): refresh Cargo lockfiles

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-26 12:51:08 -05:00
Anand Krishnamoorthi
f9d54cd436 ci(dependabot): fix cargo config quoting (#632) 2026-03-26 11:18:09 -05:00
Anand Krishnamoorthi
5b60daabd9 feat: add Azure Policy builtins with YAML test suite (#630)
* feat: add Azure Policy builtins with YAML test suite

Implement ARM template functions for Azure Policy evaluation:

Builtins:
- String: indexOf, lastIndexOf, trim, format, split, startsWith, endsWith,
  padLeft, concat, replace, toLower, toUpper, substring, guid, uniqueString
- DateTime: dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, addDays
- Collection: intersection, union, take, skip, first, last, min, max,
  range, items, tryGet, tryIndexFromEnd, empty, array, createObject
- Encoding: base64, base64ToString, base64ToJson, uri, uriComponent,
  uriComponentToString, dataUri, dataUriToString
- Numeric: int, float, intDiv, intMod
- Misc: json, join, bool, string, coalesce, if, getParameter, resolveField
- Logic: logicAll, logicAny

Key implementation details:
- Unicode case-insensitive search via ICU4X case folding with single-pass
  fold_with_char_map() for indexOf/lastIndexOf
- .NET composite formatting (System.String.Format) with alignment, standard
  and custom datetime format specifiers, numeric format specifiers
- DateTime round-trip preserves input shape (Z vs +00:00, T vs space,
  fractional seconds) when no explicit output format is supplied
- Zero-cost as_str() helper borrows directly from Value::String(Rc<str>)
- BTreeSet<&Value> in array union avoids redundant cloning

Test suite:
- 53 YAML test files exercising all builtins via direct BUILTINS registry
- Coverage for edge cases: empty inputs, Unicode, fractional seconds,
  invalid alignment, unknown format specifiers, RFC3339 offset shapes

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address PR review comments

- Fix percent_encode to only uppercase hex digits, not entire string
- Remove guid/uniqueString (unsupported); delete custom SHA-1 impl
- Replace unwrap_or(0) with proper error in format placeholder parsing
- Hoist CaseMapper into static CaseMapperBorrowed for zero per-call overhead
- Pre-allocate Vec in range() with_capacity
- Update bindings/ffi and bindings/ruby Cargo.lock
- Fix uri_component test expectations for correct case preservation

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address second round of PR review comments

- float(): return Undefined when as_f64() fails instead of leaking
  the original non-f64 representation
- createObject(): reject odd number of arguments with an error
  (ARM-template parity)
- format(): error on unknown numeric format specifiers instead of
  silently passing through (matches .NET FormatException behavior)
- format(): cap alignment width at 10,000 to prevent DoS from
  user-controlled format strings like {0,1000000000}
- Add YAML test cases for all new error behaviors

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address third round of PR review comments

- percent_decode: reject incomplete % escapes (e.g. "%", "%2") instead
  of treating them as literal characters
- parse_iso8601_duration: reject leftover digits without a unit designator
  at T boundary and end-of-input (e.g. "P1", "P1T2H")
- yaml_to_value: panic on unsupported YAML numeric representations instead
  of silently mapping to Null
- Revert unused src/languages/mod.rs changes (module is defined inline in
  lib.rs)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: add missing edge-case tests and fix empty-delimiter panic

- fn_split: return input as single-element array for empty string
  delimiter instead of panicking (Rust's str::split("") panics)
- format: add test for F3 higher precision ({0:F3} + 1.23456 → 1.235)
- format: add test for N2 float with thousands separator
- format: add test for negative index error ({-1})
- split: add test for empty-string delimiter
- uri: add tests for query string and fragment in relative URI
- createObject: add test for non-string (numeric) keys

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address fourth round of PR review comments

- Add MAX_VARIADIC_ARGS (64) constant for variadic builtin arity
  instead of registering with 0 (logic_all, logic_any, min, max,
  format, intersection, union, coalesce, createObject); set
  dateTimeAdd to exact arity 3

- Switch indexOf/lastIndexOf to UTF-16 code-unit indices to match
  .NET String.IndexOf semantics (track ch.len_utf16() in
  fold_with_char_map, use encode_utf16().count() for empty-needle
  lastIndexOf)

- Use DateTime::<Utc>::from_timestamp for explicit timezone type

- Remove stale docs/azure-policy/casing.md link from module doc

- Fix misleading comment in want_error test branch (code bails on
  Undefined, not accepts it)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-25 17:32:39 -05:00
Anand Krishnamoorthi
f69974dc1b ci(dependabot): fix cargo workspace updates and refresh lockfiles (#629)
* ci(dependabot): fix cargo workspace updates and refresh lockfiles

Remove nested Cargo workspace members from Dependabot's cargo directories to avoid manifest resolution failures during grouped updates.

Add a Dependabot-only workflow that refreshes affected Cargo lockfiles, including the no_std target-specific resolution path, so CI can continue enforcing --locked and --frozen builds.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci(dependabot): address workflow review comments

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci(dependabot): address workflow permission and toolchain comments

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci(dependabot): stage no-std lockfile refresh

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci(dependabot): harden refresh workflow

* ci(dependabot): refine workflow gating and staging

* ci(dependabot): harden workflow git operations

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-25 16:58:29 -05:00
dependabot[bot]
942dd47163 build(deps): bump rubocop in /bindings/ruby in the per-dependency group (#622)
Bumps the per-dependency group in /bindings/ruby with 1 update: [rubocop](https://github.com/rubocop/rubocop).


Updates `rubocop` from 1.85.0 to 1.85.1
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.85.0...v1.85.1)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.85.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 12:37:06 -05:00
dependabot[bot]
ac701b4933 ci(deps): bump the github-actions group with 11 updates (#628)
Bumps the github-actions group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4` | `6` |
| [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) | `2.8.2` | `2.9.1` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.2.0` | `6.3.0` |
| [actions/setup-dotnet](https://github.com/actions/setup-dotnet) | `5.1.0` | `5.2.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.2.0` | `6.3.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.32.4` | `4.34.1` |
| [ruby/setup-ruby](https://github.com/ruby/setup-ruby) | `1.288.0` | `1.295.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` |
| [PyO3/maturin-action](https://github.com/pyo3/maturin-action) | `1.50.0` | `1.50.1` |
| [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) | `0.5.127` | `0.5.128` |


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `Swatinem/rust-cache` from 2.8.2 to 2.9.1
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](779680da71...c19371144d)

Updates `actions/setup-go` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4b73464bb3)

Updates `actions/setup-dotnet` from 5.1.0 to 5.2.0
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](baa11fbfe1...c2fa09f4bd)

Updates `actions/setup-node` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](6044e13b5d...53b83947a5)

Updates `github/codeql-action` from 4.32.4 to 4.34.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](89a39a4e59...3869755554)

Updates `ruby/setup-ruby` from 1.288.0 to 1.295.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](09a7688d3b...319994f95f)

Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](b7c566a772...bbbca2ddaa)

Updates `actions/download-artifact` from 7.0.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](37930b1c2a...3e5f45b2cf)

Updates `PyO3/maturin-action` from 1.50.0 to 1.50.1
- [Release notes](https://github.com/pyo3/maturin-action/releases)
- [Commits](b1bd829e37...04ac600d27)

Updates `MarcoIeni/release-plz-action` from 0.5.127 to 0.5.128
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](f708778669...1528104d2c)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-dotnet
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: ruby/setup-ruby
  dependency-version: 1.295.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: PyO3/maturin-action
  dependency-version: 1.50.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: MarcoIeni/release-plz-action
  dependency-version: 0.5.128
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 12:35:38 -05:00
Anand Krishnamoorthi
86088d2049 Consolidate Dependabot, fix #595 (mimalloc + indexmap), add feature-matrix CI (#627)
* build: consolidate dependabot cargo entries and add commit prefixes

Consolidate all 9 separate cargo ecosystem entries into a single entry
using the 'directories' key. This ensures Dependabot creates one PR per
dependency update across the root workspace and all bindings, preventing
version skew that caused build failures.

Also add semantic commit-message prefixes to all ecosystem entries:
- build(deps) for cargo, gomod, maven, nuget, pip, bundler
- ci(deps) for github-actions

Rename the cargo group to 'rust-dependencies' and the github-actions
group to 'github-actions' for clarity.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: remove mimalloc from default features, fix indexmap/std propagation

Address #595: the vendored mimalloc allocator should not be imposed on
library consumers. Remove allocator-memory-limits and mimalloc from the
full-opa feature so that users of regorus as a library can choose their
own global allocator.

Bindings (ffi, java, python, ruby) that ship as standalone artifacts
continue to opt in to regorus/allocator-memory-limits explicitly so they
retain the performant allocator.

Also propagate indexmap/std via the std feature (using the indexmap?/std
weak-dependency syntax) so that users enabling std + rvm without default
features no longer hit 'IndexMap takes 3 generic arguments' errors.

Closes #595

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* ci: add feature-combination checks to PR CI and weekly matrix

PR CI (xtask): add cargo check for 5 non-default feature combos in
run_ci_suite(). These run on every PR and catch compile failures from
feature-gating issues (e.g. #595) with near-zero overhead.

Weekly workflow: new feature-matrix.yml runs cargo build + cargo test
across 9 feature combinations every Monday. Uses a GitHub Actions matrix
with fail-fast: false so all combos are tested even if one fails.

Combinations tested weekly:
- std,arc (minimal library)
- std,arc,rvm (common library usage)
- std,arc,full-opa (full-opa without mimalloc)
- std,arc,full-opa,allocator-memory-limits (binding-style)
- std,arc,rvm,regex,time,semver,cache (cherry-picked builtins)
- std,arc,rvm,coverage,cache (observability)
- std,arc,full-opa,azure_policy (Azure Policy)
- std,arc,full-opa,azure-rbac (Azure RBAC)
- arc,opa-no-std (no_std codepath)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: gate benchmark memory-limit calls behind allocator-memory-limits feature

The set_global_memory_limit function is only available when the
allocator-memory-limits feature is enabled. After removing mimalloc
from the default feature set, the rvm_benchmark failed to compile.

Add #[cfg(feature = "allocator-memory-limits")] guards around the
call sites and the MEMORY_LIMIT_BYTES constant.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-24 11:54:01 -05:00
Anand Krishnamoorthi
83891d7782 RVM compiler & runtime optimizations: caching, instruction fusion, constant hoisting, and correctness fixes (#626)
* perf!: add LRU caches for compiled regex and glob patterns

Add bounded LRU caches for compiled regex and glob patterns used by
Rego builtins, avoiding repeated recompilation of the same patterns
during policy evaluation.

New `cache` feature (included in `full-opa` and `opa-no-std`) backed by
the `lru` crate (no_std compatible) with `spin::Mutex` for thread safety.

- `src/cache.rs`: generic `LruCache<V>` wrapper, global `REGEX_CACHE`
  (default capacity 256) and `GLOB_CACHE` (default capacity 128)
- `src/builtins/regex.rs`: all regex builtins route through the cache
- `src/builtins/glob.rs`: glob.match routes through the cache
- Public API: `regorus::cache::{Config, configure, clear}`

Compilation costs avoided per cache hit:
  regex  10-55 µs  (simple to complex patterns)
  glob   10-12 µs
  LRU hit   ~10 ns

BREAKING CHANGE: new `cache` Cargo feature added to `full-opa` and
`opa-no-std` feature sets; adds `lru` as a dependency.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(vm): amortize per-instruction memory and time limit checks

Deduplicate per-instruction memory_check calls by hoisting them to the
main dispatch loop, and amortize monotonic_now() syscalls in the
execution timer by checking elapsed time every N instructions instead
of on every tick.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix(vm): correct object membership to check values only, not keys

The Contains instruction for objects was checking both keys and values:

    object_fields.contains_key(v) || object_fields.values().any(|v| ...)

Per the Rego specification, `x in obj` tests whether x is a VALUE of
the object, not a key. The two-argument form `k, v in obj` is needed
to access keys. The interpreter already implemented this correctly
(values-only scan), but the RVM had the extra contains_key() check
which would incorrectly return true when the search value happened to
match a key name.

Remove the contains_key() branch so the behavior matches the interpreter
and the Rego spec. Add two regression tests:
- object_membership_checks_values_not_keys: "foo" in {"foo": "bar"}
  must be false (key, not a value)
- object_membership_finds_value: "bar" in {"foo": "bar"} must be true

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): hoist all-constant collection literals to the literal table

When an array, set, or object literal consists entirely of compile-time
constant expressions (numbers, strings, bools, null, and nested constant
collections), the compiler now evaluates them at compile time and emits a
single Load instruction from the literal table instead of generating
per-element instructions at runtime.

Previously, a Rego expression like `x in [1, 2, 3]` would emit
ArrayCreate + three Load + three ArrayAppend instructions, allocating a
new Vec and Rc on every evaluation. With this change, the entire array
is built once during compilation and loaded as a single constant.

This optimization applies to all three collection types:
- Array literals: avoids ArrayCreate + N x (Load + ArrayAppend)
- Set literals: avoids SetCreate + N x (Load + SetAdd)
- Object literals: avoids ObjectCreate + N x (Load + Load + ObjectInsert)

The implementation adds a try_eval_const() helper that recursively
evaluates an AST expression as a constant Value, returning None if any
sub-expression is non-constant. Each compile method for collection
literals attempts the all-constant fast path first and falls through to
the existing instruction-by-instruction codegen otherwise.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): fuse Eq + AssertCondition into AssertEq instruction

Add a new `AssertEq { left, right }` instruction that combines equality
comparison and condition assertion into a single operation. This replaces
the previous two-instruction pattern of `Eq { dest, left, right }` followed
by `AssertCondition { condition: dest }`, saving one instruction and one
register per equality assertion.

The fused instruction checks two registers for equality and directly calls
handle_condition with the result, avoiding the intermediate boolean
register entirely. If either operand is undefined or the values differ,
the condition fails and the rule/loop backtracks.

The optimization applies to four destructuring sites:
- EqualityCheck (assignment re-binding with `x = expr; x = expr`)
- EqualityExpr (destructuring against an expression)
- EqualityValue (destructuring against a literal value)
- assert_array_length (array length validation in destructuring)

In soft_assert_mode the compiler still emits the original Eq instruction
since the boolean result register is needed by callers.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(compiler): fuse Not + AssertCondition into AssertNot instruction

Add a new `AssertNot { operand }` instruction that combines logical
negation and condition assertion into a single operation. This replaces
the previous two-instruction pattern of `Not { dest, operand }` followed
by `AssertCondition { condition: dest }`, saving one instruction and one
register allocation.

The fused instruction checks the operand register and passes the
condition if the value is false or undefined (per Rego semantics where
`not expr` succeeds when the expression has no results or is false),
and fails the condition if the value is true or any non-boolean truthy
value.

This was the only emission site for the Not+AssertCondition pair,
occurring in the compilation of `Literal::NotExpr` statements.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(vm): early exit for same-value multi-definition rules

When a rule has multiple definitions that all produce the same value
(e.g. implicit true, or identical literal), set early_exit_on_first_success
on RuleInfo so the VM can stop after the first successful definition.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* feat!: expose cache configuration API to all language bindings

Add `set_cache_config` and `clear_cache` functions to every binding
so callers can tune or reset the global regex/glob pattern caches
introduced in the cache feature.

Bindings updated:
- FFI (C): `regorus_set_cache_config`, `regorus_clear_cache`
- C++ header: free functions `regorus::set_cache_config`, `regorus::clear_cache`
- Python: module-level `set_cache_config(*, regex, glob)`, `clear_cache()`
- Java: static methods on new `CacheConfig` class
- Go: package-level `SetCacheConfig`, `ClearCache`
- Ruby: module functions `Regorus.set_cache_config`, `Regorus.clear_cache`
- WASM: free functions `setCacheConfig`, `clearCache`
- C#: static methods `Engine.SetCacheConfig`, `Engine.ClearCache`

BREAKING CHANGE: Bump SERIALIZATION_VERSION from 4 to 5 due to new
AssertEq and AssertNot instruction variants added in the instruction
fusion commits. Programs serialized with version 5 cannot be loaded
by older versions of regorus.

* fix: address PR review feedback

Cache subsystem:
- Gate REGEX_CACHE and related imports behind #[cfg(feature = "regex")]
  so that building with --features cache without regex compiles correctly.
- Gate LruCache struct behind #[cfg(any(feature = "regex", feature = "glob"))].
- Add Config::MAX_CAPACITY (2^16) hard upper bound; clamp values in
  configure() to prevent unbounded cache growth.
- Use parking_lot::Mutex for std builds and spin::Mutex for no_std to
  avoid CPU spinning under contention in tight regex/glob eval loops.
- Narrow lock scopes in regex/glob builtins: release the mutex before
  compiling a pattern, then re-acquire to insert.

Java JNI binding:
- Fix cache config overflow: negative jlong values now saturate to 0
  and positive overflow saturates to usize::MAX (then clamped by
  MAX_CAPACITY) instead of silently disabling the cache.
- Gate JNI cache config/clear functions behind #[cfg(feature = "cache")].

Compiler:
- Refactor static_value_of_expr to delegate to try_eval_const,
  gaining support for negated numbers and constant collections.
- Make try_eval_const pub(in crate::languages::rego::compiler) and
  re-export through expressions.rs.
- Handle Expr::UnaryExpr with numeric literals in try_eval_const so
  collections containing negated numbers (e.g. [-1, 2]) are hoisted.

VM correctness:
- Fix Not instruction to follow Rego semantics: not expr yields
  true when expr is undefined or false, false for any other defined
  value (including non-booleans) -- no longer errors on non-boolean
  operands.
- Add enforce_memory_check() call at execute_suspendable_entry to
  ensure memory limits are checked before the first instruction.
- Update AssertNot listing comment to "exit if any defined truthy
  value" to match actual VM behaviour.
- Add doc comment on Not instruction clarifying Rego negation
  semantics.

Bindings:
- Fix C++ header indentation for set_cache_config / clear_cache.
- Propagate Cargo.lock parking_lot addition across ffi, java, python,
  and wasm binding lockfiles.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-23 21:00:51 -05:00
antmhs
898643129e feat: make policy length limits configurable per engine (#624)
- Add PolicyLengthConfig struct with max_col, max_file_bytes, and
  max_lines fields, replacing hardcoded constants in the lexer.
- Add Engine::set_policy_length_config and clear_policy_length_config
  to allow callers to override the default limits.
- Add Source::from_contents_with_limits and from_file_with_limits for
  direct Source construction with custom limits; existing from_contents
  and from_file signatures are preserved using defaults.
- Add tests for default rejection, custom limits, and engine plumbing.
- Add bindings for C, C++, Python, WASM/JS, Java, Ruby, C#, Go
2026-03-13 12:19:57 -05:00
Anand Krishnamoorthi
50c0215fdb Rvm optimizations (#620)
* perf(rvm): fix O(n²) comprehension yield by mutating in-place

Instead of cloning the entire accumulator collection on every yield
iteration, use take_register + Rc::make_mut to get exclusive ownership
and mutate in-place. This reduces comprehension yield from O(n²) to O(n)
for both run-to-completion and suspendable execution modes.

- Add RegoVM::take_register() helper that swaps register with Undefined
- Comprehension yield now takes the accumulator, mutates via Rc::make_mut,
  and writes back — avoiding deep clones when refcount == 1

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(rvm): use take_register for ObjectSet, ArrayPush, SetAdd

These instructions were cloning the container register (bumping Rc to 2),
then calling as_object_mut/as_array_mut/as_set_mut which invokes
Rc::make_mut — deep-cloning the entire collection since refcount > 1.

Use take_register instead so the Rc refcount stays at 1, making
Rc::make_mut a no-op and allowing in-place mutation.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(rvm): remove unnecessary clones in rule caching

- execute_call_rule_common: move final_value into cache instead of
  cloning, since it is not used afterwards
- finalize_rule_frame_data: add comment clarifying the clone is needed
  because the value is both cached and returned
- Remove unnecessary .clone() on result_from_rule when setting register

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* rvm: avoid RuleInfo clone per rule call

Replace RuleInfo.clone() (which heap-allocates name, destructuring_blocks, and
potentially function_info) with a cheap Arc<Program> clone (atomic refcount
bump) followed by borrowing &RuleInfo from the local Arc. This eliminates
per-rule-call heap allocations.

Sites changed:
- execute_call_rule_common: Arc clone + borrow
- execute_call_rule_suspendable: Arc clone + borrow
- finalize_rule_frame_data: Arc clone + borrow
- handle_rule_break_event: inline Arc clone + borrow (was get_rule_info)
- handle_rule_error_event: inline Arc clone + borrow (was get_rule_info)
- Removed now-unused get_rule_info method

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* rvm: replace bincode with postcard for serialization

Remove unlinked bincode dependency. Use postcard (already a dep for rvm feature)
for all binary serialization/deserialization in program serialization and tests.

Also adds rvm_benchmark benchmark.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(rvm): cache dummy Span/Expr for builtin calls

Every builtin call was allocating a Source (via from_contents), a Span, and
N Ref<Expr> wrappers just to satisfy the builtin function signature. These
dummy values are only used for error reporting context.

Cache the dummy Span and Vec<Ref<Expr>> on the RegoVM struct. The Source and
Span are created once on first builtin call; dummy Expr entries grow as
needed and are reused across calls via mem::take/put-back pattern.

This eliminates per-builtin-call heap allocations for Source (Rc + String +
Vec<lines>), Span clones, and Rc<Expr> wrappers.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* perf(rvm): round 2 allocation reduction in builtins, entry points, virtual data

- Cache builtin args Vec on RegoVM (mem::take/clear/put-back pattern)
- Restructure builtins_cache as two-level map for clone-free lookup
- Use IndexMap::get_index() in execute_entry_point_by_index
- Use mutable Vec path stack in traverse_rule_tree_subobject (push/pop)
- Walk data tree and rule-result paths by reference, clone only leaf
- Use mem::replace in resume() instead of cloning ExecutionState

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix(rvm): address PR review feedback

- Restore cached_builtin_args on all error/early-return paths in
  execute_builtin_call to preserve allocation reuse
- Use 1-based line/col and \"<builtin>\" filename in dummy span for
  clearer diagnostics
- Restore result register before returning errors in comprehension
  mode-mismatch branches (both run-to-completion and suspendable)
- Avoid clone in resume() invalid-state error path by formatting
  debug string before moving state back

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-11 21:39:58 -05:00
Anand Krishnamoorthi
ee3dff9a3d fix(ci): skip mimalloc FFI and disable isolation for Miri (#621)
- Add cfg(not(miri)) guards to mimalloc module, global allocator, and
  allocator-memory-limits code paths so Miri falls back to the default
  system allocator instead of calling unsupported FFI functions.
- Set MIRIFLAGS="-Zmiri-disable-isolation" in the workflow so tests
  that perform file I/O can run under Miri.
- Skip units/parse tests under Miri due to Float-vs-BigInt Number
  representation mismatch with Miri's soft-float emulation.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-11 15:13:51 -05:00
dependabot[bot]
b8e15f46f3 build(deps): bump rubocop in /bindings/ruby in the per-dependency group (#618)
Bumps the per-dependency group in /bindings/ruby with 1 update: [rubocop](https://github.com/rubocop/rubocop).


Updates `rubocop` from 1.84.2 to 1.85.0
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.84.2...v1.85.0)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 15:10:45 -06:00
Anand Krishnamoorthi
37144968c8 chore(ci): add miri workflow (#581)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-05 13:18:25 -06:00
Anand Krishnamoorthi
7ee503ccdc chore(ci): add cargo audit and deny (#580)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-05 13:18:00 -06:00
Anand Krishnamoorthi
006e819d52 rvm: switch binary serialization to postcard (#582)
Move RVM binary encoding from bincode to postcard and bump the format version. Update test helpers, docs, changelog, and refresh lockfiles after the swap.

Closes #575

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-03 15:09:45 -06:00
dependabot[bot]
b6f11c5602 build(deps-dev): bump org.apache.maven.plugins:maven-surefire-plugin (#605)
Bumps the per-dependency group in /bindings/java with 1 update: [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire).


Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:52:44 -06:00
dependabot[bot]
72033e77da build(deps): bump bytes (#569)
Bumps the cargo group with 1 update in the /bindings/java directory: [bytes](https://github.com/tokio-rs/bytes).


Updates `bytes` from 1.11.0 to 1.11.1
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:52:14 -06:00
dependabot[bot]
be34063dba build(deps): bump the per-dependency group with 2 updates (#603)
Bumps the per-dependency group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action).


Updates `github/codeql-action` from 4.32.2 to 4.32.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](45cbd0c69e...9e907b5e64)

Updates `MarcoIeni/release-plz-action` from 0.5.126 to 0.5.127
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](52440b50d3...f708778669)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
- dependency-name: MarcoIeni/release-plz-action
  dependency-version: 0.5.127
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:51:21 -06:00
dependabot[bot]
04bf417c06 build(deps): bump the per-dependency group across 1 directory with 3 updates (#607)
Bumps the per-dependency group with 3 updates in the /bindings/ruby directory: [minitest](https://github.com/minitest/minitest), [rubocop](https://github.com/rubocop/rubocop) and [rubocop-minitest](https://github.com/rubocop/rubocop-minitest).


Updates `minitest` from 6.0.1 to 6.0.2
- [Changelog](https://github.com/minitest/minitest/blob/master/History.rdoc)
- [Commits](https://github.com/minitest/minitest/compare/v6.0.1...v6.0.2)

Updates `rubocop` from 1.84.1 to 1.84.2
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.84.1...v1.84.2)

Updates `rubocop-minitest` from 0.38.2 to 0.39.1
- [Release notes](https://github.com/rubocop/rubocop-minitest/releases)
- [Changelog](https://github.com/rubocop/rubocop-minitest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-minitest/compare/v0.38.2...v0.39.1)

---
updated-dependencies:
- dependency-name: minitest
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
- dependency-name: rubocop
  dependency-version: 1.84.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
- dependency-name: rubocop-minitest
  dependency-version: 0.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:50:52 -06:00
Paulo Lieuthier
bc23cd08ac Python: boolean mapping (#612)
* fix(python): boolean and integer mapping
2026-02-27 15:55:28 -06:00
Paulo Lieuthier
1c607dc1d3 feat: implement add_extension in Python binding (#596)
Add support for registering custom Python functions as Rego extensions,
allowing users to call Python callables directly from Rego policies.

The implementation:
- Converts Rego values to Python types on call, and back on return
- Validates that the extension is callable at registration time
- Wraps errors with the extension name for easier debugging
- Documents clone semantics (shared callable reference across clones)

Tests cover: basic execution, type conversions (int, float, bool, None,
list, dict, set), zero-arg extensions, wrong arity, exception
propagation, non-callable rejection, and duplicate registration.

Contributed by @paulolieuthier
2026-02-25 15:55:52 -06:00
Anand Krishnamoorthi
47cc27ff49 feat(rbac)!: add Azure RBAC engine, FFI API, and cross-language tests (#577)
- add Azure RBAC condition interpreter and builtin evaluation in core (expressions, parser updates, evaluator, and test harness)
- introduce comprehensive RBAC YAML test suites and coverage for i
  - action/suboperation
  - strings
  - numbers
  - bools
  - IP
  - GUID
  - dates
  - times
  - lists
  - quantifiers (ForAnyOfAnyValues, ForAllOfAllValues)
- expose RBAC evaluation through FFI with an `rbac` feature flag enabled by default
- add C# `RbacEngine` wrapper + P/Invoke entrypoint and document usage in C# README
- expand C# tests to execute all RBAC YAML cases with per-case logging
- wire test assets into C# test output and centralize YAML dependency versions

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-02-19 15:30:03 -06:00
dependabot[bot]
8814eda0ae Bump the per-dependency group with 1 update (#587)
Bumps Microsoft.Build.NoTargets from 3.7.56 to 3.7.134

---
updated-dependencies:
- dependency-name: Microsoft.Build.NoTargets
  dependency-version: 3.7.134
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 16:47:12 -06:00
dependabot[bot]
b4a69a13ba build(deps): bump the per-dependency group (#585)
---
updated-dependencies:
- dependency-name: magnus
  dependency-version: 0.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: regorus
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: serde_magnus
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 09:53:36 -06:00
dependabot[bot]
e83a47497a build(deps): bump the per-dependency group (#586)
Bumps the per-dependency group in /bindings/ruby with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [minitest](https://github.com/minitest/minitest) | `5.25.4` | `6.0.1` |
| [rake](https://github.com/ruby/rake) | `13.2.1` | `13.3.1` |
| [rake-compiler](https://github.com/rake-compiler/rake-compiler) | `1.2.9` | `1.3.1` |
| [rake-compiler-dock](https://github.com/rake-compiler/rake-compiler-dock) | `1.9.1` | `1.11.0` |
| [rubocop](https://github.com/rubocop/rubocop) | `1.73.2` | `1.84.1` |
| [rubocop-minitest](https://github.com/rubocop/rubocop-minitest) | `0.37.1` | `0.38.2` |
| [rb_sys](https://github.com/oxidize-rb/rb-sys) | `0.9.111` | `0.9.124` |


Updates `minitest` from 5.25.4 to 6.0.1
- [Changelog](https://github.com/minitest/minitest/blob/master/History.rdoc)
- [Commits](https://github.com/minitest/minitest/compare/v5.25.4...v6.0.1)

Updates `rake` from 13.2.1 to 13.3.1
- [Release notes](https://github.com/ruby/rake/releases)
- [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc)
- [Commits](https://github.com/ruby/rake/compare/v13.2.1...v13.3.1)

Updates `rake-compiler` from 1.2.9 to 1.3.1
- [Release notes](https://github.com/rake-compiler/rake-compiler/releases)
- [Changelog](https://github.com/rake-compiler/rake-compiler/blob/master/History.md)
- [Commits](https://github.com/rake-compiler/rake-compiler/compare/v1.2.9...v1.3.1)

Updates `rake-compiler-dock` from 1.9.1 to 1.11.0
- [Release notes](https://github.com/rake-compiler/rake-compiler-dock/releases)
- [Changelog](https://github.com/rake-compiler/rake-compiler-dock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rake-compiler/rake-compiler-dock/compare/v1.9.1...v1.11.0)

Updates `rubocop` from 1.73.2 to 1.84.1
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.73.2...v1.84.1)

Updates `rubocop-minitest` from 0.37.1 to 0.38.2
- [Release notes](https://github.com/rubocop/rubocop-minitest/releases)
- [Changelog](https://github.com/rubocop/rubocop-minitest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-minitest/compare/v0.37.1...v0.38.2)

Updates `rb_sys` from 0.9.111 to 0.9.124
- [Release notes](https://github.com/oxidize-rb/rb-sys/releases)
- [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.111...v0.9.124)

---
updated-dependencies:
- dependency-name: minitest
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: rake
  dependency-version: 13.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: rake-compiler
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: rake-compiler-dock
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: rubocop
  dependency-version: 1.84.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: rubocop-minitest
  dependency-version: 0.38.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: rb_sys
  dependency-version: 0.9.124
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 09:53:00 -06:00
dependabot[bot]
241c1d445b build(deps-dev): bump the per-dependency group (#583)
Bumps the per-dependency group in /bindings/java with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [junit:junit](https://github.com/junit-team/junit4) | `3.8.1` | `4.13.2` |
| [com.google.code.gson:gson](https://github.com/google/gson) | `2.10.1` | `2.13.2` |
| [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) | `3.1.0` | `3.6.3` |
| [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.2.5` | `3.5.4` |
| [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) | `3.6.3` | `3.12.0` |
| [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) | `3.3.0` | `3.4.0` |


Updates `junit:junit` from 3.8.1 to 4.13.2
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.2.md)
- [Commits](https://github.com/junit-team/junit4/commits/r4.13.2)

Updates `com.google.code.gson:gson` from 2.10.1 to 2.13.2
- [Release notes](https://github.com/google/gson/releases)
- [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/gson/compare/gson-parent-2.10.1...gson-parent-2.13.2)

Updates `org.codehaus.mojo:exec-maven-plugin` from 3.1.0 to 3.6.3
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/exec-maven-plugin-3.1.0...3.6.3)

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.2.5 to 3.5.4
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.2.5...surefire-3.5.4)

Updates `org.apache.maven.plugins:maven-javadoc-plugin` from 3.6.3 to 3.12.0
- [Release notes](https://github.com/apache/maven-javadoc-plugin/releases)
- [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.6.3...maven-javadoc-plugin-3.12.0)

Updates `org.apache.maven.plugins:maven-source-plugin` from 3.3.0 to 3.4.0
- [Release notes](https://github.com/apache/maven-source-plugin/releases)
- [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.0...maven-source-plugin-3.4.0)

---
updated-dependencies:
- dependency-name: junit:junit
  dependency-version: 4.13.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: com.google.code.gson:gson
  dependency-version: 2.13.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: org.codehaus.mojo:exec-maven-plugin
  dependency-version: 3.6.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: org.apache.maven.plugins:maven-javadoc-plugin
  dependency-version: 3.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: per-dependency
- dependency-name: org.apache.maven.plugins:maven-source-plugin
  dependency-version: 3.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 09:46:33 -06:00
dependabot[bot]
4054d1b6b6 build(deps): bump the per-dependency group with 12 updates (#593)
Bumps the per-dependency group with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.3.1` | `6.0.2` |
| [actions/setup-python](https://github.com/actions/setup-python) | `5.6.0` | `6.2.0` |
| [actions/setup-java](https://github.com/actions/setup-java) | `4.8.0` | `5.2.0` |
| [actions/setup-go](https://github.com/actions/setup-go) | `5.1.0` | `6.2.0` |
| [actions/setup-dotnet](https://github.com/actions/setup-dotnet) | `4.1.0` | `5.1.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `4.4.0` | `6.2.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.32.2` | `4.32.2` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `6.0.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `5.0.0` | `7.0.0` |
| [PyO3/maturin-action](https://github.com/pyo3/maturin-action) | `63b75c597b83e247fbf4fb7719801cc4220ae9f3` | `b1bd829e37fef14c63f19162034228a2f3dc1021` |
| [MarcoIeni/release-plz-action](https://github.com/marcoieni/release-plz-action) | `0.5.108` | `0.5.126` |
| [oxidize-rb/actions](https://github.com/oxidize-rb/actions) | `1.2.6` | `1.4.4` |


Updates `actions/checkout` from 4.3.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](34e114876b...de0fac2e45)

Updates `actions/setup-python` from 5.6.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a26af69be9...a309ff8b42)

Updates `actions/setup-java` from 4.8.0 to 5.2.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4.8.0...be666c2fcd27ec809703dec50e508c2fdc7f6654)

Updates `actions/setup-go` from 5.1.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5.1.0...7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5)

Updates `actions/setup-dotnet` from 4.1.0 to 5.1.0
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4.1.0...baa11fbfe1d6520db94683bd5c7a3818018e4309)

Updates `actions/setup-node` from 4.4.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](49933ea528...6044e13b5d)

Updates `github/codeql-action` from 3.32.2 to 4.32.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3.32.2...45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2)

Updates `actions/upload-artifact` from 4.6.2 to 6.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...b7c566a772)

Updates `actions/download-artifact` from 5.0.0 to 7.0.0
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](634f93cb29...37930b1c2a)

Updates `PyO3/maturin-action` from 63b75c597b83e247fbf4fb7719801cc4220ae9f3 to b1bd829e37fef14c63f19162034228a2f3dc1021
- [Release notes](https://github.com/pyo3/maturin-action/releases)
- [Commits](63b75c597b...b1bd829e37)

Updates `MarcoIeni/release-plz-action` from 0.5.108 to 0.5.126
- [Release notes](https://github.com/marcoieni/release-plz-action/releases)
- [Commits](8724d33cd9...52440b50d3)

Updates `oxidize-rb/actions` from 1.2.6 to 1.4.4
- [Release notes](https://github.com/oxidize-rb/actions/releases)
- [Commits](7ca44a16e2...e5f9a49a78)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/setup-java
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/setup-go
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/setup-dotnet
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/setup-node
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: github/codeql-action
  dependency-version: 4.32.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/upload-artifact
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: actions/download-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: per-dependency
- dependency-name: PyO3/maturin-action
  dependency-version: b1bd829e37fef14c63f19162034228a2f3dc1021
  dependency-type: direct:production
  dependency-group: per-dependency
- dependency-name: MarcoIeni/release-plz-action
  dependency-version: 0.5.126
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: per-dependency
- dependency-name: oxidize-rb/actions
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: per-dependency
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 06:30:19 -06:00
Anand Krishnamoorthi
8f7ca44bdf chore(dependabot): expand coverage and pin workflows (#579)
- Expand dependabot coverage across Rust subcrates and other ecosystems.

- Group updates per dependency and ignore vendored mimalloc crates.

- Pin GitHub Actions to exact SHAs in existing workflows.
2026-02-11 17:43:48 -06:00
Anand Krishnamoorthi
96360fa9d8 fix(bindings): add SafeHandleWrapper + memory growth checks; bump 0.9.1 (#571)
- Introduce SafeHandleWrapper with gating, short drain wait, and deferred release on last in-flight exit.
- Wire Engine/Program/Rvm/CompiledPolicy to wrapper (centralized handle use, interop helper).
- Add C# memory growth tests (using/finalizer paths) and extend xtask C# runner options.
- Add pooled marshalling utilities, ResultHelpers, and API cleanups; update versions/changelog.

Fixes #570. Closes #554

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-02-09 12:22:05 -06:00
Anand Krishnamoorthi
455d2aa588 chore(nuget): Add support for macosx (#553)
Additionally
- Include more metadata in nuget package
- Also generate snupkg for native symbols.
  We intentionally don't add the symbols for native rust shared library
  to the nuget package since that could increase the size of the nuget.
  We will revisit that later.
- update licenses of all the bindings.

closes #551

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-02-03 00:09:52 +05:30
Elijah Koulaxis
0e5fe9b9ac feat: add tests for number semantics (#555) 2026-01-31 07:27:51 +05:30
Anand Krishnamoorthi
0e9e34a519 chore: Gate exports using allocator-limits feature (#564)
This is needed to publish regorus-mimalloc

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-30 23:56:29 +05:30
Anand Krishnamoorthi
3f7a5496dc feat(bindings)!: add RVM/Program support across FFI and language bindings (#565)
- FFI: add RVM/Program APIs, execution state accessors, HostAwait handling, and buffer/result helpers in rvm.rs, common.rs, engine.rs.
- Compiler: emit HostAwait for __builtin_host_await in function_calls.rs.
- RVM tests: add HostAwait regression cases and extend harness for suspend/resume responses in host_await.yaml and mod.rs.
- C/C++: add RVM tests/examples and wrapper updates in rvm_tests.c, rvm_tests.cpp, regorus.hpp, plus CMake wiring.
- C#: add Program/Rvm bindings, SafeHandle/PInvoke, tests, and example usage in Regorus, RvmProgramTests.cs, Program.cs, and README updates.
- Go: add Program/Rvm bindings, tests, and examples in rvm.go, rvm_test.go, main.go.
- Java: add Program/Rvm bindings, JNI glue, and examples in lib.rs, regorus, Test.java.
- Python: add Program/Rvm bindings and examples in lib.rs, test.py.
- WASM: add Program/Rvm bindings and examples in lib.rs, test.js.
- Tooling: wire binding tests in xtask and ignore generated Java artifacts in .gitignore.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-30 23:55:31 +05:30
Anand Krishnamoorthi
0316ccd90c chore(release): publish vendored mimalloc crates (#563)
- Prefix regorus- to mimalloc crates and add MIT licenses
- alias dependencies to avoid code changes
- add versions and release-plz publish entries
- update Cargo.lock files for new crate names

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-30 09:03:02 +05:30
Anand Krishnamoorthi
10eebfe54c test(rvm): Move vm execution limit tests to a separate test to avoid flakiness (#558)
Having a separate integration test allows the execution tests to freely
change the global fallback limits without affecting other tests.

also ask release-plz to ignore xtask package

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-29 04:15:33 +05:30
Anand Krishnamoorthi
e688806ca0 chore: Keep regorus and binding versions in sync (#552)
Bump up the versions to 0.9.0 to match the C# binding version.

Also use central version management for C# projects

Also fix clippy lint errors

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-28 05:58:34 +05:30
Anand Krishnamoorthi
394625d4bc feat!: add cooperative execution-time limits across engine, VM, and binding (#539)
- Introduce ExecutionTimer/ExecutionTimerConfig to allow limiting evaluating time.
- To amortize time checking costs, checking interval can be configured via the notion of work units
- A global fallback time limit can be set to universally limit all evaluation in addition to engine level limit setting.
- Implement limnits in interpreter and RVM. In RVM, also handle suspend/resume so that time during pause is not counted.
- Add engine-level APIs to set/clear per-engine timer configuration and apply global fallback defaults.
- Surface execution-time limits through FFI and C# bindings
- Add C# tests and example usage to validate engine overrides, global fallback behavior, and compiled policy enforcement.
- Expand docs for execution-time limit
- Add interpreter YAML cases and VM unit tests for time-limit behavior and deterministic time sources.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-28 05:58:03 +05:30
Anand Krishnamoorthi
e68e852ee3 feat(xtask): consolidate CI workflows onto xtask helpers (#542)
- split the xtask crate into structured modules for
  - bindings
  - ci
  - dev
  - util
  - no-std
- Adding commands for
  - ci-release/ci-debug
  - MUSL/no-std
  - per- binding language smoke tests
  - developer tasks (fmt, clippy, pre-commit, pre-push)
- refresh Cargo manifests/locks, binding readmes, and shared FFI helpers so every binding reuses the same preparation steps
- refactor GitHub Actions (release/debug, extensions, CodeQL, clippy, bindings) to call the new xtask commands
- Use rust-cache in ci workflows (microsoft qdk also does this)
- extend README with a contributor workflow section describing how xtask mirrors CI expectations
- update pre-commit and pre-push hooks to use the xtask dev commands

WORKAROUND:
When dotnet is run from an xtask, codeql tracer intercepts it an routes to a nonexistent binary.
Therefore in codeql workflow, xtask is not used for c# and instead dotnet is directly invoked.
Tracked by #545

closes #475

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-27 07:42:21 +05:30
Anand Krishnamoorthi
2b1434b3ac fix(release): Fix release-plz dirty-tree errors from Ruby binstubs (#547)
Ruby binstubs were checked in, but ignore via .gitignore **bin pattern.
This causes release-plz to think that the source tree is dirty.

The binstubs are deleted from source repo since they are always regenerated by bundler.
Also simplizy release-plz to focus only on the core crate.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-24 07:09:25 +05:30
Anand Krishnamoorthi
fd59bb5a91 feat(memory): Allocator-backed global memory limits (#544)
Policy evaluation at scale needs to be able to set memory limits
so that a bad policy does not hog memory or to ensure that
policy evaluation itself does not use too much memory which could
cause other components to suffer.

This PR introduces capability to set and enforce global memory limits.
It also lays the groundwork for enabling per evaluation limits in future.

Once a global memory limit is set, Regorus maintains per thread counters
to track memory activity (allocation, deallocation) of a thread.
These counters are periodically flushed to global memory counters.
Per thread counters avoid the contention that updating global counters
on each alloc/free would cause.

Policy evaluation periodically checks these counters and raises errors
if allocated memory has exceeded the configured limit.

Currently memory limit capability is exposed only to FFI and C#.

Also update mimalloc to v2.2.6

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-24 07:08:54 +05:30
Anand Krishnamoorthi
80686d6ed1 feat(ffi): unwind safety: shield FFI entrypoints with panic guard (#546)
This PR implements widely accepted Rust programming practices for
dealing with panics across ABI (programming language) boundaries.

- Add panic_guard.rs to wrap FFI calls and prevent panic across FFI/ABI boundary (undefined behavior).
- Capture per-thread backtraces via a temporary panic hook
- After a panic, subsequent invocations are poisoned.
- Integrate with_unwind_guard across the engine, schema registry, and target registry exportis

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-21 09:20:10 +05:30
Anand Krishnamoorthi
9426b2ec02 fix: Imports without a name binding (#543)
Handle imports that don't use the `as` clause to create a binding.
These imports are bound to the last identifier in the imported path.

Fix both interpreter and compiler.
Add tests.

fixes #541

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-15 03:57:52 +05:30
Anand Krishnamoorthi
740db8a0f5 chore: Harden RVM implementation (#537)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-14 06:07:31 +05:30
Ben Beasley
d626f75421 Include all licenses in SPDX expression and LICENSE file (#540)
Change the SPDX license expression from `MIT` to
`MIT AND Apache-2.0 AND BSD-3-Clause` to reflect all of the licenses
that apply to the crate’s sources.

Add license text for `Apache-2.0` and the `BSD-3-Clause` license from
Go’s `time` module to `LICENSE`. Like `MIT`, both of these licenses
require the license text to be distributed with source and/or binaries.
2026-01-10 18:31:49 +05:30
Anand Krishnamoorthi
5afbd96159 chore: Interpreter hardening (#536)
Fix majority of the interpreter lint errors/warnings

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-30 18:21:09 -06:00
Anand Krishnamoorthi
28891ef883 chore: Harden instructions and program (#535)
Also enforce sane limits in program

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-30 18:20:07 -06:00
Anand Krishnamoorthi
49958c2ece chore: Make clippy clean and harden helpers (#532)
- Promote common accessors (Expr/Rule span/eidx, ScopeContext constructors, Engine::set_rego_v0) to const
- Prefer Option combinators (map_or, then_some) and map_or_else
- Tighten engine logic: add missing semicolons, use checked u32::try_from, make boolean query evaluation avoid unchecked indexing,

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-29 14:47:09 -06:00
Anand Krishnamoorthi
08a5e00960 chore: Harden lexer bounds and span handling (#531)
- Document arithmetic safety assumptions and add explicit lexer limits for columns, file size (1 MiB), and line count.
  Realistic policies will be well within these bounds.
- Use checked arithmetic to prevent overflow underflow.
- Avoid var name shadowing.
- Misc clippy lints

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-29 14:46:05 -06:00
Anand Krishnamoorthi
1d71df30b6 chore: Fix lint errors in lookup.rs (#530)
- Made the lookup module crate-visible to address clippy’s redundant visibility lint.
- Replaced unchecked as casts with a fallible usize_from_u32 helper and propagate conversion errors in lookup accessors.
- Switched LookupIndexError to implement core::error::Error for no_std correctness.
- Fixed the pattern type mismatch by matching on the value in the Display impl.
- Promoted trivial helpers to const fn (new, module_len) per clippy suggestions.
- Centralized bounds-checked slot access via slot_ref/slot_mut to keep getters/clearers lint-clean and avoid unchecked indexing.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-29 13:53:46 -06:00
Anand Krishnamoorthi
249dcd0b43 chore: Add clippy lints (#529)
Lints are added (deny) at crate level.

In each offending file, the failing lints are explicitly allowed.
Each file will be fixed in subsequent PRs.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-23 15:59:34 -06:00
Anand Krishnamoorthi
604591a0f7 Merge pull request #527 from anakrish/checked-indexing
Checked indexing
2025-12-19 13:24:55 -06:00
Anand Krishnamoorthi
dbfb8e38a8 fix: Skip reordering in truncated queries.
In case all the statements of a query don't execute,
skip reordering the result expressions to match the
source order. Doing so requires maintaining additional
data structures not worth the complexity for now.

Additionally we want to discourage queries and encourage
evaluating rules. Queries are inherently less performant
than rules which can be precompiled.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 12:08:09 -06:00
Anand Krishnamoorthi
273a80571e fix: apply expression ordering to schedule in a safe way
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:25 -06:00
Anand Krishnamoorthi
3f29eb2fa6 fix: Create ordered statements in a safe way
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:25 -06:00
Anand Krishnamoorthi
889a02ddd6 fix: Avoid unwrap when accesssing current module
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
70f63a0982 fix: Avoid unrap/expect in context management
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
6bc1249dc8 feat: Safeguard lookup use
Detect invalid indexes and raise internal errors.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
5d0cf95332 feat: add recursion limit to parser
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
fd4bb3081f feat: Safeguard against panics in parser
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
93a633750c feat: Guard against runtime panics in lexer
Add guardrails for operations to ensure that they
won't panic at runtime.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-19 11:33:24 -06:00
Anand Krishnamoorthi
52b56f4214 Merge pull request #526 from anakrish/quick-fixes
Quick fixes
2025-12-17 16:56:06 -06:00
Anand Krishnamoorthi
8b84d4ce12 Merge pull request #525 from tjons/tjons/feat-implement-net-cidr-expand
feat: implement `net.cidr_expand` builtin
2025-12-17 13:16:52 -06:00
Anand Krishnamoorthi
ecf95833f9 Merge pull request #519 from anakrish/opa-rvm-3
Opa rvm 3
2025-12-16 14:11:08 -06:00
Anand Krishnamoorthi
9fa8036ce4 feat: Implement Rego else block compilation
- teach the Rego compiler to compile else chains correctly
- test suite

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 12:03:03 -06:00
Anand Krishnamoorthi
a232b13e50 feat: Else blocks in definitions
- ensure both run-to-completion and suspendable rule execution stop evaluating
  bodies once one succeeds so later else branches are skipped
- test cases

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 12:03:03 -06:00
Anand Krishnamoorthi
e9a50bcfd5 test: skip test case which queries rule suffixes
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 12:03:02 -06:00
Anand Krishnamoorthi
d0fa639bb8 feat: Reject with keyword usage
RVM does not plan to support the `with` keyword which is mainly used
for testing.

- introduce CompilerError::WithKeywordUnsupported and fail query compilation
  when any literal carries with_mods
- skip OPA test cases that hit the error

The "withkeyword" folder is retained in the TODO list to indicate its
lack of support.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 12:03:02 -06:00
Anand Krishnamoorthi
a514e8da83 fix: Implement RVM set ops correctly
- treat set subtraction in RVM the same as the interpreter by supporting
  Value::Set operands in sub_values
- emit internal-only builtin names for set union/intersection and register
  handlers so compiled bytecode resolves without exposing new Rego builtins
- add regression coverage for literal set difference/intersection
  (x/y from failure.rego) in tests/rvm/rego/cases/sets.yaml

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 12:03:02 -06:00
Anand Krishnamoorthi
252ae0e312 Merge pull request #516 from anakrish/rvm-opa-2
Handle more OPA semantics in RVM and compiler
2025-12-16 11:28:36 -06:00
Anand Krishnamoorthi
ce85e0102d fix: use debug build in pre-commit hook
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 09:55:20 -06:00
Anand Krishnamoorthi
a8f5ac6117 fix: Run pre-commit tests in debug mode
Release mode uses LTO optimization for binaries.
This can take up a lot of time especially for doc tests which
create a separate binary for each test.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 09:43:57 -06:00
Anand Krishnamoorthi
632f64b2ce fix: remove non-existent feature use in git push hook
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-16 09:36:24 -06:00
tjons
c41f289b19 feat: implement net.cidr_expand builtin
Signed-off-by: tjons <tylerschade99@gmail.com>
2025-12-16 06:05:32 -05:00
Anand Krishnamoorthi
2a75b3b0b6 Merge pull request #524 from hkrutzer/fix-underflow
fix: Integer underflow in lexer error message formatting
2025-12-15 14:37:11 -06:00
Hans Krutzer
3962b3c38d fix: Integer underflow in lexer error message formatting 2025-12-15 20:00:45 +01:00
Anand Krishnamoorthi
d4b7d1ff6c Merge pull request #522 from microsoft/dependabot/cargo/criterion-0.8.1
build(deps): bump criterion from 0.7.0 to 0.8.1
2025-12-10 06:38:27 -06:00
Anand Krishnamoorthi
d6cd738822 Merge pull request #523 from dpokluda/jetbrains-gitignore-support
Add support for JetBrains IDEs gitignore
2025-12-08 17:13:56 -06:00
David Pokluda
36e75d3e49 Add support for JetBrains IDEs gitignore 2025-12-08 14:17:20 -08:00
dependabot[bot]
befe131048 build(deps): bump criterion from 0.7.0 to 0.8.1
Bumps [criterion](https://github.com/criterion-rs/criterion.rs) from 0.7.0 to 0.8.1.
- [Release notes](https://github.com/criterion-rs/criterion.rs/releases)
- [Changelog](https://github.com/criterion-rs/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/criterion-rs/criterion.rs/compare/criterion-plot-v0.7.0...criterion-v0.8.1)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 02:07:45 +00:00
Anand Krishnamoorthi
bedf667adc feat: Handle literal comparisons that use = and comprehensions without loops
- emit AssertCondition for equality-only assignment plans (outside soft-assert mode) so rules like `0 = 1` fail under the VM just like the interpreter
- let comprehension bodies consume assertion failures by advancing or exiting their iteration context, both in run-to-completion and suspendable execution

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-03 13:34:14 -06:00
Anand Krishnamoorthi
8269968c4a feat: Handle computed reference roots in RVM compiler
Allow compile_chained_ref to fall back to “evaluate root expression → chain access”
so literal arrays, comprehensions, and other computed roots no longer raise NotSimpleReferenceChain.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-03 13:34:14 -06:00
Anand Krishnamoorthi
e3d23766ae feat: Ensure RVM caches deterministic builtins
Mirror interpreter implementation:
- use builtins::must_cache to determine whether builtin must be cached.
- reuse cached value when applicable
- clear the VM’s builtin cache whenever execution state resets to avoid leaking values across runs
- add a YAML regression for rand.intn set comprehensions and re-enable the rand cases in the OPA test suite

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-03 13:34:14 -06:00
Anand Krishnamoorthi
1d627f3798 Merge pull request #515 from anakrish/rvm-opa-1
feat: Handle more OPA semantics in RVM compiler
2025-12-03 13:33:04 -06:00
Anand Krishnamoorthi
b7b3d3ec87 feat: Soft-assert mode for builtin out-params under not
- Add a scoped soft_assert_mode to the compiler so `not` statements compile their subexpressions without emitting hard AssertCondition/AssertNotUndefined instructions.
- Teach binding-plan application to return an optional result register; equality plans now yield a boolean in soft mode, allowing not abs(-5 , 3) to succeed instead of aborting.
- Update function-call, loop, and rule plumbing to consume the new binding-plan outcome, including copying the produced register when an out-parameter equality is used.
- Trim the OPA TODO list to the remaining troublesome folders.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-02 15:27:43 -06:00
Anand Krishnamoorthi
30bd134a0b fix: Handle builtin out-parameter calls in RVM compiler
- Teach the hoister/destructuring planner to respect parent scope when building binding plans for extra arguments, so already-bound vars yield equality checks.
- Update the compiler’s function-call path to drop the trailing out-argument, run its binding plan after the call, and share call-target resolution logic.
- Add regression suites for builtin and user-defined out-parameter scenarios plus align the CLI example output when RVM returns undefined.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-02 13:29:12 -06:00
Anand Krishnamoorthi
5aefd51cb6 feat: Add span information to compiler errors
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-02 13:27:38 -06:00
Anand Krishnamoorthi
e060e43a6c test: OPA RVM validation (#514)
- fixes:
  - ensure loop hoist lookups reserve query capacity and keep loop-var tables sized when compiling default rules
  - rebuild hoisting tables with the analyzer’s schedule when available so statement order matches evaluation

- OPA test
  - Also test using RVM workflow in OPA suite
  - Maintain a list of test folders that don't yet pass and skip them

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-02 13:26:38 -06:00
Anand Krishnamoorthi
12c083e29e test: Add RVM compiler testing to ACI tests (#509)
code fixes:
  - compiler: add `is_var_bound_in_current_scope` and use it in destructuring so
    only the innermost scope blocks rebinding while still catching duplicates
    within that block.
  - rvm: treat `not` over undefined operands as a successful negation to match
    interpreter semantics.

tests/aci:
   migrate YAML cases to `data.policy.rule` queries with `{x: …}`
  bindings, expand the harness to run interpreter plus RVM (with optional
  skipping), align results to the binding format, add readable timing output,
  and support a `--filter` flag for targeting cases.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-01 16:58:44 -06:00
Anand Krishnamoorthi
a8a3a9809b feat!: Use num-bigint for large numbers (#500)
- Supply chain: Use the popular num-bigint crate for handling large integers
- Optimization: Handle f64, i64, u64 directly. These will be the most common instances of a number.

OPA number semantics isn't clear.
https://github.com/open-policy-agent/opa/issues/6281

As part of this change, we update the following failing tests:
- A local test that relies on what 15.3/3 evaluates to.
 With our current change, we round in a different direction than what OPA does, but consistent
 with Rust. We produce 5.1000000000000005 where as the OPA test expects 5.1.
 There is no clear definition in Rego of what the right answer is. Moreover, policies should not
 rely on exact floating point value comparison. Therefore this deviations is justified.
 The test is patched to pass.
- Another local vm test that exercised 1.1 + 2.2
- Another local vm test that exercises 5.5 - 2.2
- An OPA test that expects that a large integer number say 10e308 is printed in exponent notation.
 num-bigint does not print using scientific notation and instead prints all the digits.
 The benefit of preserving this compatibility is not clear. We skip this test.
- Doc tests that exercised handling floating point numbers with more than 15 (what f64 supports)
  digits of precision. There is no usecase for this scenario. The tests are updated to reflect
  the behavior.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2025-12-01 13:25:02 -06:00
Copilot
14deaaa5b6 Re-enable wasm-pack test after upstream issue fix (#508)
* Initial plan

* Re-enable wasm-pack test and update Node.js to v22

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2025-11-25 12:03:16 -06:00
569 changed files with 65585 additions and 10098 deletions

View File

@@ -4,7 +4,7 @@ inputs:
toolchain:
description: 'Rust toolchain version'
required: false
default: '1.89.0'
default: '1.92.0'
components:
description: 'Additional components to install'
required: false

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
@@ -5,7 +7,95 @@
version: 2
updates:
# All Rust/Cargo directories are grouped into a single entry so that
# when a dependency is updated, Dependabot bumps it across the root
# workspace AND every binding, preventing version skew.
- package-ecosystem: "cargo"
directory: "/" # Location of package manifests
directories:
- "/"
- "/bindings/ffi"
- "/bindings/java"
- "/bindings/python"
- "/bindings/ruby"
- "/bindings/wasm"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
# Bundle all Cargo dependency updates into a single PR. Without this,
# dependabot creates a separate PR per directory for the same dependency,
# and each individual PR fails to build due to version skew.
rust-dependencies:
patterns:
- "*"
# Ignore vendored mimalloc crates; updates are managed manually.
ignore:
- dependency-name: "regorus-mimalloc"
- dependency-name: "regorus-mimalloc-sys"
- package-ecosystem: "gomod"
directory: "/bindings/go"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "maven"
directory: "/bindings/java"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "nuget"
directory: "/bindings/csharp"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "pip"
directory: "/bindings/python"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "bundler"
directory: "/bindings/ruby"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci(deps)"
groups:
github-actions:
patterns:
- "*"

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: "CodeQL Security Analysis"
on:
@@ -60,46 +62,65 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Setup language-specific dependencies BEFORE CodeQL init for proper tracing setup
- name: Setup Rust
if: matrix.language == 'rust' || matrix.language == 'c-cpp'
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch workspace dependencies
run: cargo fetch --locked
- name: Fetch FFI crate dependencies
if: matrix.language == 'c-cpp' || matrix.language == 'go' || matrix.language == 'csharp'
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- name: Fetch Java crate dependencies
if: matrix.language == 'java-kotlin'
run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml
- name: Setup Python
if: matrix.language == 'python'
uses: actions/setup-python@v5
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- name: Setup Java
if: matrix.language == 'java-kotlin'
uses: actions/setup-java@v4
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'corretto'
java-version: '8'
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.21'
- name: Setup .NET
if: matrix.language == 'csharp'
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
- name: Invoke dotnet directly
if: matrix.language == 'csharp'
run: dotnet --info
- name: Setup Node.js
if: matrix.language == 'javascript-typescript'
uses: actions/setup-node@v4
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '18'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -120,7 +141,7 @@ jobs:
- name: Setup Ruby
if: matrix.language == 'rust' && contains(matrix.working-directory, 'ruby')
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
with:
ruby-version: '3.4.2'
bundler-cache: true
@@ -132,81 +153,41 @@ jobs:
cargo install wasm-pack
# Manual build steps for different languages
- name: Build C/C++ FFI bindings
- name: Build C/C++ bindings via xtask
if: matrix.language == 'c-cpp'
working-directory: ${{ matrix.working-directory }}
run: |
# Build FFI library in no_std mode for embedded/constrained environments
cargo build --release --locked --features "ast,coverage,regorus/opa-no-std" --no-default-features
cargo xtask test-c --release --frozen
cargo xtask test-cpp --release --frozen --skip-ffi
cargo xtask test-c-no-std --release --frozen --skip-ffi
# Build the Rust FFI library that provides C-compatible interface
cargo build --release --locked
# Build C bindings using CMake
cd ../c
mkdir -p build
cd build
cmake ..
make
# Build C++ bindings using CMake
cd ../../cpp
mkdir -p build
cd build
cmake ..
make
- name: Build Java bindings
- name: Build Java bindings via xtask
if: matrix.language == 'java-kotlin'
working-directory: ${{ matrix.working-directory }}
run: |
# Build the Rust JNI library that provides Java-compatible interface
cargo fetch
cargo build --release --locked
# Compile Java source and create JAR package with Maven
mvn package
run: cargo xtask test-java --release --frozen
- name: Build Go bindings
- name: Build Go bindings via xtask
if: matrix.language == 'go'
working-directory: ${{ matrix.working-directory }}
run: |
# Build the FFI library that Go bindings depend on via CGO
cd ../ffi
cargo fetch
cargo build --release --locked
cd ../go
# Download Go dependencies
go mod tidy
# Set up environment for CGO linking to Rust FFI library
export CGO_ENABLED=1
export LD_LIBRARY_PATH="$(pwd)/../ffi/target/release:$LD_LIBRARY_PATH"
# Build Go packages with verbose output for CodeQL tracing
go build -v ./pkg/regorus
go build -v -o regorus_test .
run: cargo xtask test-go --release --frozen
- name: Build C# bindings
- name: Build C# bindings manually
if: matrix.language == 'csharp'
working-directory: ${{ matrix.working-directory }}
run: |
# Temporary workaround: CodeQL's tracer replaces dotnet with a missing shim when cargo xtask test-csharp runs,
# so invoke dotnet directly here until the upstream fix lands.
# Ideal command once fixed: cargo xtask test-csharp --release
# Build the FFI library that C# bindings access via P/Invoke
cd ../ffi
cargo fetch
cargo build --release --locked
cd ../csharp
# Restore NuGet packages and build .NET assemblies in release mode
# Build the main Regorus library project only (tests require packaged version)
dotnet restore Regorus/Regorus.csproj
dotnet build Regorus/Regorus.csproj --no-restore /p:Configuration=Release /p:IgnoreMissingArtifacts=true
- name: Build WASM bindings
- name: Build WASM bindings via xtask
if: matrix.language == 'javascript-typescript'
working-directory: ${{ matrix.working-directory }}
run: |
# Build WebAssembly module with wasm-pack for Node.js target
cargo fetch
wasm-pack build --target nodejs --release
run: cargo xtask build-wasm --release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"

View File

@@ -0,0 +1,129 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: dependabot/refresh-cargo-lockfiles
on:
pull_request_target:
types: [opened, synchronize, reopened]
branches: ["main"]
concurrency:
group: dependabot-refresh-cargo-lockfiles-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
refresh-cargo-lockfiles:
permissions:
contents: write
if: >-
github.event.pull_request.user.login == 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
persist-credentials: false
- name: Setup Rust toolchain
run: |
rustup toolchain install 1.92.0 --profile minimal
rustup override set 1.92.0
cargo --version
rustc --version
- name: Refresh affected Cargo lockfiles
shell: bash
run: |
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
mapfile -t changed_files < <(git diff --name-only "$base_sha" "$head_sha" -- ':(glob)**/Cargo.toml' ':(glob)**/Cargo.lock')
if [ "${#changed_files[@]}" -eq 0 ]; then
echo "No Cargo manifest or lockfile changes detected."
exit 0
fi
declare -A manifests=()
for path in "${changed_files[@]}"; do
case "$path" in
bindings/ffi/*)
manifests["bindings/ffi/Cargo.toml"]=1
;;
bindings/java/*)
manifests["bindings/java/Cargo.toml"]=1
;;
bindings/python/*)
manifests["bindings/python/Cargo.toml"]=1
;;
bindings/ruby/*)
manifests["bindings/ruby/Cargo.toml"]=1
;;
bindings/wasm/*)
manifests["bindings/wasm/Cargo.toml"]=1
;;
*)
manifests["Cargo.toml"]=1
;;
esac
done
for manifest in "${!manifests[@]}"; do
echo "Refreshing lockfile for $manifest"
cargo metadata \
--config 'build.rustc="rustc"' \
--config 'build.rustc-wrapper=""' \
--config 'build.rustc-workspace-wrapper=""' \
--format-version 1 \
--all-features \
--manifest-path "$manifest" > /dev/null
done
if [[ -n "${manifests[Cargo.toml]+x}" ]]; then
echo "Refreshing lockfile for tests/ensure_no_std/Cargo.toml (thumbv7m-none-eabi)"
cargo metadata \
--config 'build.rustc="rustc"' \
--config 'build.rustc-wrapper=""' \
--config 'build.rustc-workspace-wrapper=""' \
--format-version 1 \
--manifest-path tests/ensure_no_std/Cargo.toml \
--filter-platform thumbv7m-none-eabi > /dev/null
fi
- name: Commit lockfile refresh
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mapfile -t lockfiles < <(git ls-files -m -o --exclude-standard -- ':(glob)**/Cargo.lock')
for lockfile in "${lockfiles[@]}"; do
git add "$lockfile"
done
if git diff --cached --quiet; then
echo "No Cargo lockfile changes required."
exit 0
fi
auth_header=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
trap 'git config --unset-all http.https://github.com/.extraheader' EXIT
git config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "build(deps): refresh Cargo lockfiles"
git push origin HEAD:${{ github.event.pull_request.head.ref }}

66
.github/workflows/dependency-audit.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Dependency Audits
on:
pull_request:
push:
branches: ["main"]
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
cargo-audit:
name: Cargo Audit (${{ matrix.lockfile }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
lockfile:
- Cargo.lock
- bindings/ffi/Cargo.lock
- bindings/java/Cargo.lock
- bindings/python/Cargo.lock
- bindings/ruby/Cargo.lock
- bindings/wasm/Cargo.lock
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run cargo audit
uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
lockfile: ${{ matrix.lockfile }}
cargo-deny:
name: Cargo Deny (${{ matrix.manifest }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
manifest:
- Cargo.toml
- bindings/ffi/Cargo.toml
- bindings/java/Cargo.toml
- bindings/python/Cargo.toml
- bindings/ruby/Cargo.toml
- bindings/ruby/ext/regorusrb/Cargo.toml
- bindings/wasm/Cargo.toml
- tests/ensure_no_std/Cargo.toml
- xtask/Cargo.toml
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Rust
uses: ./.github/actions/toolchains/rust
- name: Run cargo deny
uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
command-arguments: advisories bans
manifest-path: ${{ matrix.manifest }}

82
.github/workflows/feature-matrix.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# Thorough weekly test of non-default feature combinations.
# Catches regressions from dependency updates and feature-gating issues
# that the fast PR CI checks (cargo check only) would miss at runtime.
name: tests/feature-matrix
on:
workflow_dispatch:
schedule:
# Run at 3:42 AM UTC every Saturday.
- cron: "42 3 * * 6"
env:
CARGO_TERM_COLOR: always
jobs:
feature-matrix:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# Bare minimum: validates that the core interpreter works
# without any builtins or optional subsystems.
- name: minimal (std + arc)
features: std,arc
# Common library usage pattern (issue #595): consumer enables
# std + arc + rvm and relies on indexmap/std propagation.
- name: library (std + arc + rvm)
features: std,arc,rvm
# New default after removing mimalloc from full-opa.
# Ensures all builtins compile without the allocator.
- name: full-opa (no mimalloc)
features: std,arc,full-opa
# Binding-style usage: full-opa with the vendored allocator.
# Mirrors how ffi/java/python/ruby bindings are built.
- name: full-opa + allocator
features: std,arc,full-opa,allocator-memory-limits
# Selective builtins without full-opa: validates that popular
# features can be cherry-picked independently.
- name: cherry-picked builtins
features: std,arc,rvm,regex,time,semver,cache
# Observability features only: coverage + cache without the
# heavier builtins (regex, time, etc.).
- name: observability
features: std,arc,rvm,coverage,cache
# Azure Policy adds jsonschema + dashmap; test it compiles
# and runs on top of full-opa.
- name: azure-policy
features: std,arc,full-opa,azure_policy
# Azure RBAC adds regex + time + net on top of full-opa.
- name: azure-rbac
features: std,arc,full-opa,azure-rbac
# no_std with the OPA-compatible feature set: exercises the
# spin_no_std codepath and absence of std-only dependencies.
- name: no_std
features: arc,opa-no-std
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus-features
- name: Fetch dependencies
run: cargo fetch --locked
- name: Build
run: cargo build --no-default-features --features "${{ matrix.features }}" --frozen
- name: Test
run: cargo test --no-default-features --features "${{ matrix.features }}" --frozen

29
.github/workflows/miri.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: miri
on:
workflow_dispatch:
schedule:
# Run at 6:30 AM UTC every Wednesday
- cron: "30 6 * * 3"
jobs:
miri-test:
name: miri (nightly)
runs-on: ubuntu-latest
env:
MIRIFLAGS: "-Zmiri-disable-isolation"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: ./.github/actions/toolchains/rust
with:
toolchain: nightly
components: miri rust-src
- name: Set up Miri
run: cargo miri setup
- name: Run Miri tests
run: cargo miri test -p regorus
- name: Run Miri ACI tests
run: cargo miri test -p regorus --test aci
- name: Run Miri kata tests
run: cargo miri test -p regorus --test kata

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: tests/release-extensions
on:
@@ -18,21 +20,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Build only std
run: cargo build -r --example regorus --no-default-features --features "std,rego-extensions"
- name: Doc Tests
run: cargo test -r --doc --features rego-extensions
- name: Run tests
run: cargo test -r --features rego-extensions
- name: Run example
run: cargo run --example regorus --features rego-extensions -- eval -d examples/server/allowed_server.rego -i examples/server/input.json data.example
- name: Run tests (ACI)
run: cargo test -r --test aci --features rego-extensions
- name: Run tests (KATA)
run: cargo test -r --test kata --features rego-extensions
- name: Run tests (OPA Conformance)
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Run rego extensions CI suite
run: >-
cargo test -r --test opa --features opa-testutil,serde_json/arbitrary_precision,rego-extensions -- $(tr '\n' ' ' < tests/opa.passing)
cargo xtask ci-release --frozen --features rego-extensions
--skip-all-features-build --skip-no-default-features-tests
--skip-azure-policy --skip-azure-rbac
--opa-features "opa-testutil,serde_json/arbitrary_precision,rego-extensions"

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: tests/release
on:
@@ -18,36 +20,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Format Check
run: cargo fmt --check
- name: Fetch
run: cargo fetch
- name: Build (all features)
run: cargo build -r --all-features --frozen
- name: Build
run: cargo build -r --frozen
- name: Test no_std
run: cargo test -r --no-default-features --frozen
- name: Build only std
run: cargo build -r --example regorus --no-default-features --features "std" --frozen
- name: Doc Tests
run: cargo test -r --doc --frozen
- name: Run tests
run: cargo test -r --frozen
- name: Run example
run: cargo run --example regorus --frozen -- eval -d examples/server/allowed_server.rego -i examples/server/input.json data.example
- name: Run tests (ACI)
run: cargo test -r --test aci --frozen
- name: Run tests (KATA)
run: cargo test -r --test kata --frozen
- name: Run tests (OPA Conformance)
run: >-
cargo test -r --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing)
- name: Run tests (Azure Policy)
run: >-
cargo test --frozen --features azure_policy
- name: Run tests (Azure RBAC)
run: cargo test -r --frozen --features azure-rbac
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Run release CI suite
run: cargo xtask ci-release --frozen

View File

@@ -1,6 +1,9 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: publish-java
on: workflow_dispatch
on:
workflow_dispatch:
permissions:
contents: read
@@ -32,10 +35,10 @@ jobs:
os: windows-latest
extension: dll
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
@@ -43,16 +46,17 @@ jobs:
with:
targets: ${{ matrix.target }}
- if: ${{ matrix.build_cmd == 'zigbuild' }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- if: ${{ matrix.build_cmd == 'zigbuild' }}
run: pip install cargo-zigbuild
- run: cargo fetch
- run: cargo fetch --locked
- run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml
- run: cargo ${{ matrix.build_cmd || 'build' }} --release --frozen --target ${{ matrix.target }}${{ matrix.glibc && format('.{0}', matrix.glibc) || '' }} --manifest-path ./bindings/java/Cargo.toml
- run: mkdir -p native/${{ matrix.target }}
- run: mv target/${{ matrix.target }}/release/*.${{ matrix.extension }} ./native/${{ matrix.target }}/
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: native-libraries-${{ matrix.target }}
path: native/
@@ -62,24 +66,24 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
server-id: ossrh
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: native-libraries-*
merge-multiple: true
path: ./bindings/java/native/
- run: mvn package
working-directory: ./bindings/java
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: built-jars
path: ./bindings/java/target/regorus-java-*.jar

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# This file is autogenerated by maturin v1.4.0
# To update, run
#
@@ -18,28 +20,28 @@ jobs:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- uses: ./.github/actions/toolchains/rust
- name: Build Python extension
run: |
cargo fetch
cargo fetch --locked
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo build --release --target ${{ matrix.target }} --frozen
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels-linux-${{ matrix.target }}
path: dist
@@ -50,8 +52,8 @@ jobs:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
@@ -59,19 +61,19 @@ jobs:
- name: Build Python extension
run: |
cargo fetch
cargo fetch --locked
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo build --release --target ${{ matrix.host.target }} --frozen
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --frozen --strip
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels-windows-${{ matrix.target }}
path: dist
@@ -82,27 +84,27 @@ jobs:
matrix:
target: [x86_64, aarch64, universal2-apple-darwin]
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- uses: ./.github/actions/toolchains/rust
- name: Build Python extension
run: |
cargo fetch
cargo fetch --locked
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo build --release --target ${{ matrix.host.target }} --frozen
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels-macos-${{ matrix.host.target }}
path: dist
@@ -114,13 +116,13 @@ jobs:
# if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos]
steps:
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: wheels-*
merge-multiple: true
path: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:

View File

@@ -1,10 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: publish-wasm
permissions:
pull-requests: write
contents: write
on: workflow_dispatch
on:
workflow_dispatch:
jobs:
publish-wasm:
@@ -12,11 +15,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'

View File

@@ -1,10 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: Release-plz
permissions:
pull-requests: write
contents: write
on: workflow_dispatch
on:
workflow_dispatch:
jobs:
release-plz:
@@ -14,13 +17,13 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Run release-plz
uses: MarcoIeni/release-plz-action@8724d33cd97b8295051102e2e19ca592962238f5 #v0.5.108
uses: MarcoIeni/release-plz-action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5.128
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
@@ -30,28 +32,27 @@ jobs:
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Install required cargo
run: cargo install clippy-sarif sarif-fmt
- name: Fetch
run: cargo fetch
run: cargo fetch --locked
- name: Run rust-clippy
run:
cargo clippy
--all-features
--frozen
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
continue-on-error: true
run: cargo xtask clippy --sarif rust-clippy-results.sarif
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@c298edae2d512d807fe4bdc57c0ac5a036f61501 # v3.29.11
if: ${{ hashFiles('rust-clippy-results.sarif') != '' }}
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.11
with:
sarif_file: rust-clippy-results.sarif
wait-for-processing: true

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/c-cpp
on:
@@ -14,41 +16,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- name: Setup gcc, g++, cmake, ninja
run: sudo apt update && sudo apt install -y gcc g++ cmake ninja-build
- name: Workaround to ensure that regorus.h is generated
run: |
cargo fetch
cargo build -r --frozen
working-directory: ./bindings/ffi
- name: Test c binding
run: |
mkdir bindings/c/build
cd bindings/c/build
cmake -G Ninja ..
ninja
./regorus_test
- name: Test C binding via xtask
run: cargo xtask test-c --release --frozen
- name: Test c-nostd binding
run: |
mkdir bindings/c-nostd/build
cd bindings/c-nostd/build
cmake -G Ninja ..
ninja
./regorus_test
- name: Test C (no-std) binding via xtask
run: cargo xtask test-c-nostd --release --frozen --skip-ffi
- name: Test cpp binding
run: |
mkdir bindings/cpp/build
cd bindings/cpp/build
cmake -G Ninja ..
ninja
./regorus_test
- name: Test C++ binding via xtask
run: cargo xtask test-cpp --release --frozen --skip-ffi

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/csharp
on:
@@ -31,36 +33,33 @@ jobs:
target: x86_64-unknown-linux-gnu
libpath: |
**/release/libregorus_ffi.so
# Disabled for now
#- os: macos-latest
# target: aarch64-apple-darwin
# libpath: |
# **/release/libregorus_ffi.dylib
- os: macos-latest
target: aarch64-apple-darwin
libpath: |
**/release/libregorus_ffi.dylib
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Fetch crates
run: cargo fetch
working-directory: ./bindings/ffi
with:
targets: ${{ matrix.runtime.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Check Regorus binding formatting
run: cargo fmt --check
working-directory: ./bindings/ffi
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml --target ${{ matrix.runtime.target }}
- name: Check Clippy linting for Regorus binding
run: cargo clippy --frozen -- -D warnings
working-directory: ./bindings/ffi
- name: Build Regorus binding
run: cargo build -r --target ${{ matrix.runtime.target }} --locked
working-directory: ./bindings/ffi
- name: Build Regorus FFI via xtask
run: cargo xtask build-ffi --release --target ${{ matrix.runtime.target }}
- name: Upload regorus ffi shared library
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: regorus-ffi-artifacts-${{ matrix.runtime.target }}
# Note: The full path of each artifact relative to . is preserved.
@@ -68,23 +67,32 @@ jobs:
if-no-files-found: error
retention-days: 1
build-nuget:
build-csharp:
name: 'Build Regorus nuget'
runs-on: ubuntu-latest
needs: build-ffi
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Download regorus ffi shared libraries
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: regorus-ffi-artifacts-*
merge-multiple: true
@@ -93,26 +101,22 @@ jobs:
- name: Display regorus ffi artifacts
run: ls -R ./bindings/csharp/Regorus/tmp
# Note that we need to supply the target folder within the folder where artifacts are downloaded.
- name: Build Regorus binding
run: dotnet build /p:Configuration=Release /p:RegorusFFIArtifactsDir=./tmp/bindings/ffi/target
working-directory: ./bindings/csharp/Regorus
- name: Pack
run: dotnet pack /p:RegorusFFIArtifactsDir=./tmp/bindings/ffi/target
working-directory: ./bindings/csharp/Regorus
- name: Build Regorus nuget via xtask
run: cargo xtask build-csharp --release --clean --artifacts-dir ./bindings/csharp/Regorus/tmp/bindings/ffi/target --enforce-artifacts --repository-commit ${{ github.sha }} --include-symbols
- name: Upload Regorus nuget
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: regorus-nuget
path: bindings/csharp/Regorus/bin/Release/Regorus*.nupkg
path: |
bindings/csharp/Regorus/bin/Release/Regorus*.nupkg
bindings/csharp/Regorus/bin/Release/Regorus*.snupkg
if-no-files-found: error
retention-days: 1
test-nuget:
name: 'Test Regorus Nuget: (${{ matrix.runtime.target }})'
needs: build-nuget
needs: build-csharp
runs-on: ${{ matrix.runtime.os }}
strategy:
# let us get failures from other jobs even if one fails
@@ -123,56 +127,40 @@ jobs:
target: x86_64-pc-windows-msvc
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
#- os: macos-latest
# target: aarch64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ./bindings/csharp/global.json
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Download regorus nuget
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: regorus-nuget
path: ./bindings/csharp/regorus-nuget/
path: ./bindings/csharp/Regorus/bin/Release
- name: Restore Regorus.Tests
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/Regorus.Tests
- name: Display regorus nuget
run: ls -R ./bindings/csharp/Regorus/bin/Release
- name: Run Regorus.Tests
run: dotnet test --no-restore
working-directory: ./bindings/csharp/Regorus.Tests
- name: Restore TestApp
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/TestApp
- name: Build TestApp
run: dotnet build --no-restore
working-directory: ./bindings/csharp/TestApp
- name: Run TestApp
run: dotnet run --no-build --framework net8.0
working-directory: ./bindings/csharp/TestApp
- name: Restore TargetExampleApp
run: dotnet restore /p:RestoreAdditionalProjectSources=../regorus-nuget
working-directory: ./bindings/csharp/TargetExampleApp
- name: Build TargetExampleApp
run: dotnet build --no-restore
working-directory: ./bindings/csharp/TargetExampleApp
- name: Run TargetExampleApp
run: dotnet run --no-build --framework net8.0
working-directory: ./bindings/csharp/TargetExampleApp
- name: Run C# tests via xtask
run: cargo xtask test-csharp --release --clean --nuget-dir bindings/csharp/Regorus/bin/Release

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/ffi
on:
@@ -14,15 +16,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- name: Test FFI
run: |
cargo fetch
cargo build -r --frozen
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo test --features contention_checks --frozen
working-directory: ./bindings/ffi
run: cargo xtask test-ffi --release --frozen

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/go
on:
@@ -14,23 +16,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
architecture: x64
- name: Build ffi
run: cargo build -r
working-directory: ./bindings/ffi
- name: Test go
run: |
go mod tidy
go build
LD_LIBRARY_PATH=../ffi/target/release ./regorus_test
working-directory: ./bindings/go
- name: Test Go binding via xtask
run: cargo xtask test-go --release --frozen

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/java
on:
@@ -14,34 +16,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 8
distribution: "corretto"
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Building binding
run: |
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo build --release --manifest-path bindings/java/Cargo.toml --locked
- name: Fetch Java crate dependencies
run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml
- name: Capture binding version
run: |
version=$(cargo metadata --manifest-path bindings/java/Cargo.toml --format-version 1 \
| jq -r '.packages[] | select(.name == "regorus-java") | .version')
echo "REGORUS_JAVA_VERSION=$version" >> "$GITHUB_ENV"
- name: Build jar
run: mvn package
working-directory: ./bindings/java
- name: Test jar
run: |
jar="regorus-java-${REGORUS_JAVA_VERSION}.jar"
javac -cp "target/${jar}" Test.java
java -Djava.library.path=target/release -cp "target/${jar}:." Test
working-directory: ./bindings/java
- name: Run Java smoke tests via xtask
run: cargo xtask test-java --release --frozen

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: musl
on:
@@ -18,22 +20,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/toolchains/rust
with:
targets: x86_64-unknown-linux-musl
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch MUSL target dependencies
run: cargo fetch --locked --target x86_64-unknown-linux-musl
- name: Install musl-gcc
run: sudo apt update && sudo apt install -y musl-tools
- name: Fetch
run: cargo fetch
- name: Build (MUSL)
run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl --frozen
- name: Run tests (MUSL)
run: cargo test -r --verbose --target x86_64-unknown-linux-musl --frozen
- name: Run tests (MUSL ACI)
run: cargo test -r --test aci --target x86_64-unknown-linux-musl --frozen
- name: Run tests (KATA ACI)
run: cargo test -r --test kata --target x86_64-unknown-linux-musl --frozen
- name: Run tests (MUSL OPA Conformance)
run: >-
cargo test -r --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision --target x86_64-unknown-linux-musl -- $(tr '\n' ' ' < tests/opa.passing)
- name: Run MUSL suite via xtask
run: cargo xtask test-musl --release --frozen --target x86_64-unknown-linux-musl

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/no-std
on:
@@ -18,13 +20,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/toolchains/rust
with:
targets: thumbv7m-none-eabi
- name: Fetch
run: cargo fetch
- name: Build
run: cargo build -r --target thumbv7m-none-eabi --frozen
working-directory: ./tests/ensure_no_std
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch ensure_no_std crate dependencies
run: cargo fetch --locked --manifest-path tests/ensure_no_std/Cargo.toml --target thumbv7m-none-eabi
- name: Test no-std
run: cargo xtask test-no-std --release --frozen

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/python
on:
@@ -9,9 +11,6 @@ on:
# Run at 8:00 AM every day
- cron: "0 8 * * *"
env:
PYTHON_VERSION: "3.10"
jobs:
build:
strategy:
@@ -24,70 +23,69 @@ jobs:
runs-on: ${{ matrix.host.name }}
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
targets: ${{ matrix.host.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch Python crate dependencies
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml --target ${{ matrix.host.target }}
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"
architecture: x64
- name: Build Python extension
run: |
cargo fetch
cargo clippy --all-targets --no-deps -- -Dwarnings
cargo build --release --target ${{ matrix.host.target }} --frozen
working-directory: bindings/python
- name: Install maturin
run: python -m pip install maturin==1.5.1
- name: Build Wheel
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
with:
target: x86_64
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
sccache: 'true'
- name: Build Python wheel via xtask
run: cargo xtask build-python --release --target ${{ matrix.host.target }} --target-dir bindings/python/dist --frozen
- name: Upload Wheel
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- name: Upload wheel artefacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: regorus-wheel-${{ matrix.host.name }}
path: dist/regorus-*.whl
path: bindings/python/dist/regorus-*.whl
test:
needs: build
strategy:
matrix:
host: [ubuntu-24.04, ubuntu-22.04, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
host:
- name: ubuntu-24.04
wheel: regorus-0.5.0-cp310-abi3-manylinux_2_34_x86_64.whl
- name: ubuntu-22.04
wheel: regorus-0.5.0-cp310-abi3-manylinux_2_34_x86_64.whl
- name: windows-latest
wheel: regorus-0.5.0-cp310-abi3-win_amd64.whl
needs: build
runs-on: ${{ matrix.host.name }}
runs-on: ${{ matrix.host }}
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Download Regorus wheel
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
path: wheels
pattern: regorus-wheel-*
merge-multiple: true
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- name: Fetch Python crate dependencies
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Test Wheel
run: |
pip3 install ../../wheels/${{ matrix.host.wheel }}
python3 test.py
working-directory: bindings/python
- name: Install maturin
run: python -m pip install maturin==1.5.1
- name: Run Python smoke tests via xtask
run: cargo xtask test-python --release --python python

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/ruby
on:
@@ -12,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Ruby and Rust
uses: oxidize-rb/actions/setup-ruby-and-rust@7ca44a16e287e5ff7dd72ab53f4bd41cbf34a571 #v1.26
uses: oxidize-rb/actions/setup-ruby-and-rust@e5f9a49a7812a078584072f6e3f657ad247c8771 # v1.26
with:
bundler: 2.6.5
rubygems: 3.6.5
@@ -27,10 +29,16 @@ jobs:
cargo-cache: true
working-directory: "bindings/ruby"
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch Ruby crate dependencies
run: cargo fetch --locked --manifest-path bindings/ruby/Cargo.toml
- name: Run ruby tests
run: |
cd bindings/ruby
gem install bundler
bundle install
cargo clippy --all-targets --no-deps -- -Dwarnings
bundle exec rake
run: cargo xtask test-ruby --release --frozen

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: bindings/wasm
on:
@@ -14,28 +16,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Fetch WASM crate dependencies
run: cargo fetch --locked --manifest-path bindings/wasm/Cargo.toml
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 18
node-version: 22
- name: Install wasmlpack
run: cargo install wasm-pack
- name: Test wasm binding
run: |
cd bindings/wasm
cargo fetch
cargo clippy --all-targets --no-deps -- -Dwarnings
wasm-pack build --target nodejs --release
# Enable when upstream issue is fixed.
# https://github.com/microsoft/regorus/issues/371
# wasm-pack test --release --node
node test.js
- name: Test wasm binding via xtask
run: cargo xtask test-wasm --release --frozen --node node

View File

@@ -1,3 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
#
name: tests/debug
on:
@@ -18,29 +20,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Fetch
run: cargo fetch
- name: Build (all features)
run: cargo build --all-features --frozen
- name: Build
run: cargo build --frozen
- name: Test no_std
run: cargo test --no-default-features --frozen
- name: Build only std
run: cargo build --example regorus --no-default-features --features "std" --frozen
- name: Doc Tests
run: cargo test --doc --frozen
- name: Run tests
run: cargo test --frozen
- name: Run tests (ACI)
run: cargo test --test aci --frozen
- name: Run tests (KATA)
run: cargo test --test kata --frozen
- name: Run tests (OPA Conformance)
run: >-
cargo test --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing)
- name: Run tests (Azure RBAC)
run: cargo test --frozen --features azure-rbac
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
run: cargo fetch --locked
- name: Run debug CI suite
run: cargo xtask ci-debug --frozen

23
.gitignore vendored
View File

@@ -25,12 +25,35 @@ bindings/ffi/regorus.ffi.hpp
bindings/*/target
# Temporary commit message files
.commit-msg.txt
# Local planning docs
docs/plans/
# C# build folders
**bin
**obj
bindings/csharp/.nuget/
# Bundler binstubs regenerated during ruby setup
bindings/ruby/bin/
# Visual Studio folders
**/*.vs
# Visual Studio solution files
*.sln
# JetBrains IDEs files
.idea/
# Java build artifacts
**/*.class
**/*.jar
bindings/java/.classpath
bindings/java/.project
bindings/java/.settings/
# Emacs temporary files
*~

View File

@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Azure RBAC condition interpreter with builtin evaluation coverage and YAML test suite, including quantifier (ForAnyOfAnyValues/ForAllOfAllValues), datetime (DateTimeEquals), IP (IpInRange), GUID (GuidEquals), list (ListContains), and string (StringEquals) semantics.
- FFI surface for Azure RBAC condition evaluation (see bindings changelog for language-specific wrappers).
### Changed
- [**breaking**] Switch RVM binary serialization to postcard, bump the format to v4, and mark v1-3 loads as partial (recompile required).
## [0.9.1](https://github.com/microsoft/regorus/compare/regorus-v0.9.0...regorus-v0.9.1) - 2026-02-06
### Fixed
- Release native C# handles reliably to avoid memory growth ([#571](https://github.com/microsoft/regorus/pull/571)).
- Centralize C# handle gating with a short dispose wait and deferred release to avoid leaks while blocking new calls ([#571](https://github.com/microsoft/regorus/pull/571)).
### Added
- Manual C# memory growth tests for both `using` and finalizer paths ([#571](https://github.com/microsoft/regorus/pull/571)).
- C# test runner options for filtered tests, console logging, and skipping sample apps ([#571](https://github.com/microsoft/regorus/pull/571)).
## [0.5.0](https://github.com/microsoft/regorus/compare/regorus-v0.4.0...regorus-v0.5.0) - 2025-07-08
### Added

991
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,17 @@ members = [
[package]
name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.5.0"
version = "0.9.1"
edition = "2021"
license = "MIT"
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
repository = "https://github.com/microsoft/regorus"
keywords = ["interpreter", "no_std", "opa", "policy-as-code", "rego"]
# Enable verification with Verus
[package.metadata.verus]
verify = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
@@ -22,10 +27,10 @@ doctest = false
[features]
default = ["full-opa", "arc", "rvm"]
arc = ["scientific/arc"]
arc = []
ast = []
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
azure-rbac = []
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"]
azure-rbac = ["regex", "time", "net"]
base64 = ["dep:data-encoding"]
base64url = ["dep:data-encoding"]
coverage = []
@@ -39,9 +44,11 @@ net = ["dep:ipnet"]
no_std = ["lazy_static/spin_no_std"]
opa-runtime = []
regex = ["dep:regex"]
rvm = ["dep:bincode", "dep:indexmap"]
cache = ["dep:lru"]
rvm = ["dep:postcard", "dep:indexmap"]
semver = ["dep:semver"]
std = ["rand/std", "rand/std_rng", "serde_json/std", "msvc_spectre_libs" ]
allocator-memory-limits = ["std", "mimalloc", "mimalloc/allocator-memory-limits"]
std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot" ]
time = ["dep:chrono", "dep:chrono-tz"]
uuid = ["dep:uuid"]
urlquery = ["dep:url"]
@@ -55,10 +62,10 @@ full-opa = [
"hex",
"http",
"jsonschema",
"mimalloc",
"net",
"opa-runtime",
"regex",
"cache",
"semver",
"std",
"time",
@@ -94,47 +101,56 @@ opa-testutil = []
rand = ["dep:rand"]
[dependencies]
anyhow = { version = "1.0.45", default-features = false }
anyhow = { version = "1.0.102", default-features = false }
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
hashbrown = { version = "0.16", default-features = false, features = ["default-hasher"], optional = true }
lazy_static = { version = "1.4.0", default-features = false }
thiserror = { version = "2.0", default-features = false }
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
scientific = { version = "0.5.3", default-features = false }
num-bigint = { version = "0.4", default-features = false }
num-traits = { version = "0.2", default-features = false }
parking_lot = { version = "0.12", optional = true }
spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] }
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true }
regex = {version = "1.11.1", optional = true, default-features = false }
regex = {version = "1.12.3", optional = true, default-features = false }
semver = {version = "1.0.25", optional = true, default-features = false }
url = { version = "2.5.4", optional = true }
uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.30.0", default-features = false, optional = true }
chrono = { version = "0.4.40", optional = true }
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.45.0", default-features = false, optional = true }
chrono = { version = "0.4.44", optional = true }
chrono-tz = { version = "0.10.1", optional = true }
ipnet = { version = "2.11.0", optional = true, default-features = false }
ipnet = { version = "2.12.0", optional = true, default-features = false }
icu_casemap = { version = "2.1", optional = true, default-features = false, features = ["compiled_data"] }
serde_yaml = {version = "0.9.16", default-features = false, optional = true }
# Specify thread_rng for in order to use random_range
rand = { version = "0.9.0", default-features = false, features = ["thread_rng"], optional = true }
rand = { version = "0.10.0", default-features = false, features = ["thread_rng"], optional = true }
# Causes the project to link with the Spectre-mitigated CRT and libs.
msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true }
dashmap = { version = "6.1", default-features = false, optional = true }
mimalloc = { path = "mimalloc", optional = true }
lru = { version = "0.16", default-features = false, optional = true }
mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.6", optional = true }
# rvm related deps
indexmap = { version = "2.12.1", default-features = false, features = ["serde"], optional = true }
bincode = { version = "2.0.1", default-features = false, features = ["alloc", "serde"], optional = true }
postcard = { version = "1.1.3", default-features = false, features = ["alloc"], optional = true }
# Use Verus for verification
vstd = { version = "0.0.0-2026-03-17-2326" }
[dev-dependencies]
anyhow = "1.0.45"
anyhow = "1.0.102"
cfg-if = "1.0.0"
clap = { version = "4.5.53", features = ["derive"] }
prettydiff = { version = "0.9.0", default-features = false }
serde_yaml = "0.9.16"
test-generator = "0.3.1"
walkdir = "2.3.2"
criterion = { version = "0.7" }
criterion = { version = "0.8" }
num_cpus = "1.16"
@@ -185,6 +201,16 @@ harness = false
name = "aci_benchmark"
harness = false
[[bench]]
name = "rvm_benchmark"
harness = false
required-features = ["rvm"]
[[bench]]
name = "normalization_benchmark"
harness = false
required-features = ["azure_policy"]
[[example]]
name="regorus"
harness=false
@@ -196,3 +222,6 @@ doctest=false
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(verus_keep_ghost)'] }

235
LICENSE
View File

@@ -19,3 +19,238 @@
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
The file src/builtins/time/diff.rs contains code derived from Go's `time`
package, which carries the following license:
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Some files are licensed Apache-2.0 (LICENSE-2.0.txt).
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -3,7 +3,7 @@
**Regorus** is
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
interpreter written in Rust.
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
Regorus is also
@@ -274,6 +274,19 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso
Range (min … max): 43.8 ms … 46.7 ms 62 runs
```
## Contributor Workflow
Regorus uses a small companion CLI under the `xtask` package to keep CI and local development in sync.
The commands mirror our GitHub Actions jobs, making it easy to dry-run CI steps before sending a pull request.
- Run the full release pipeline with `cargo xtask ci-release` and the debug checks with `cargo xtask ci-debug`.
- Exercise language bindings through focused helpers such as `cargo xtask test-java --release --frozen` or `cargo xtask test-go`.
- Use `cargo xtask test-musl --release --frozen` for the cross-compilation matrix and `cargo xtask test-no-std` for embedded targets.
- Formatting (`cargo xtask fmt`) and linting (`cargo xtask clippy --sarif`) wrap the usual Cargo tooling while matching CI defaults.
The workflows in `.github/workflows` invoke the same commands, so keeping local runs green is usually enough to satisfy the checks enforced on `main`.
## OPA Conformance
Regorus has been verified to be compliant with [OPA v1.2.0](https://github.com/open-policy-agent/opa/releases/tag/v1.2.0)
@@ -304,7 +317,6 @@ The following test suites don't pass fully due to missing builtins:
- `jwtverifyhs512`
- `jwtverifyrsa`
- `netcidrcontainsmatches`
- `netcidrexpand`
- `netcidrintersects`
- `netcidrmerge`
- `netcidroverlap`

View File

@@ -0,0 +1,560 @@
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use regorus::languages::azure_policy::aliases::{denormalizer, normalizer, AliasRegistry};
use regorus::Value;
use serde_json::json;
// ─── Alias catalog (reused across benchmarks) ───────────────────────────────
const ALIASES_JSON: &str = r#"[
{
"namespace": "Microsoft.Network",
"resourceTypes": [
{
"resourceType": "networkSecurityGroups",
"aliases": [
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
"defaultPath": "properties.securityRules[*].properties.protocol",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
"defaultPath": "properties.securityRules[*].properties.access",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority",
"defaultPath": "properties.securityRules[*].properties.priority",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction",
"defaultPath": "properties.securityRules[*].properties.direction",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix",
"defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange",
"defaultPath": "properties.securityRules[*].properties.destinationPortRange",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
"defaultPath": "properties.securityRules[*].name",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules[*].protocol",
"defaultPath": "properties.defaultSecurityRules[*].properties.protocol",
"paths": []
}
]
}
]
},
{
"namespace": "Microsoft.Storage",
"resourceTypes": [
{
"resourceType": "storageAccounts",
"aliases": [
{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/accessTier",
"defaultPath": "properties.accessTier",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
"defaultPath": "properties.isHnsEnabled",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
"defaultPath": "properties.minimumTlsVersion",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
"defaultPath": "properties.allowBlobPublicAccess",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/sku.name",
"defaultPath": "sku.name",
"paths": []
}
]
}
]
}
]"#;
fn build_registry() -> AliasRegistry {
let mut reg = AliasRegistry::new();
reg.load_from_json(ALIASES_JSON).unwrap();
reg
}
/// Convert a serde_json::Value to regorus::Value.
fn to_regorus(v: serde_json::Value) -> Value {
Value::from(v)
}
// ─── Input resources ────────────────────────────────────────────────────────
fn simple_storage_resource() -> Value {
to_regorus(json!({
"name": "myStorageAccount",
"type": "Microsoft.Storage/storageAccounts",
"location": "westus2",
"kind": "StorageV2",
"sku": { "name": "Standard_LRS", "tier": "Standard" },
"tags": { "environment": "production", "team": "platform" },
"properties": {
"supportsHttpsTrafficOnly": true,
"accessTier": "Hot",
"isHnsEnabled": false,
"minimumTlsVersion": "TLS1_2",
"allowBlobPublicAccess": false
}
}))
}
fn nsg_resource(rule_count: usize) -> Value {
let rules: Vec<serde_json::Value> = (0..rule_count)
.map(|i| {
json!({
"name": format!("rule-{}", i),
"properties": {
"protocol": "Tcp",
"access": if i % 2 == 0 { "Allow" } else { "Deny" },
"priority": 100 + i,
"direction": "Inbound",
"sourceAddressPrefix": format!("10.0.{}.0/24", i % 256),
"destinationPortRange": format!("{}", 80 + i)
}
})
})
.collect();
to_regorus(json!({
"name": "myNsg",
"type": "Microsoft.Network/networkSecurityGroups",
"location": "eastus",
"properties": {
"securityRules": rules
}
}))
}
// ─── Benchmarks ─────────────────────────────────────────────────────────────
fn bench_normalize_simple(c: &mut Criterion) {
let registry = build_registry();
let resource = simple_storage_resource();
c.bench_function("normalize/simple_storage", |b| {
b.iter(|| normalizer::normalize(black_box(&resource), Some(&registry), None))
});
}
fn bench_normalize_no_aliases(c: &mut Criterion) {
let resource = simple_storage_resource();
c.bench_function("normalize/simple_no_aliases", |b| {
b.iter(|| normalizer::normalize(black_box(&resource), None, None))
});
}
fn bench_normalize_nsg_scaling(c: &mut Criterion) {
let registry = build_registry();
let mut group = c.benchmark_group("normalize/nsg_rules");
for rule_count in [5, 20, 100] {
let resource = nsg_resource(rule_count);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&resource,
|b, res| b.iter(|| normalizer::normalize(black_box(res), Some(&registry), None)),
);
}
group.finish();
}
fn bench_denormalize_simple(c: &mut Criterion) {
let registry = build_registry();
let resource = simple_storage_resource();
let normalized = normalizer::normalize(&resource, Some(&registry), None);
c.bench_function("denormalize/simple_storage", |b| {
b.iter(|| denormalizer::denormalize(black_box(&normalized), Some(&registry), None))
});
}
fn bench_denormalize_nsg_scaling(c: &mut Criterion) {
let registry = build_registry();
let mut group = c.benchmark_group("denormalize/nsg_rules");
for rule_count in [5, 20, 100] {
let resource = nsg_resource(rule_count);
let normalized = normalizer::normalize(&resource, Some(&registry), None);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&normalized,
|b, norm| b.iter(|| denormalizer::denormalize(black_box(norm), Some(&registry), None)),
);
}
group.finish();
}
fn bench_round_trip(c: &mut Criterion) {
let registry = build_registry();
let resource = nsg_resource(20);
c.bench_function("round_trip/nsg_20_rules", |b| {
b.iter(|| {
let n = normalizer::normalize(black_box(&resource), Some(&registry), None);
denormalizer::denormalize(&n, Some(&registry), None)
})
});
}
fn bench_normalize_and_wrap(c: &mut Criterion) {
let registry = build_registry();
let resource = nsg_resource(20);
let context = to_regorus(json!({"resourceGroup": {"name": "rg1"}}));
let parameters = to_regorus(json!({"env": "prod"}));
c.bench_function("normalize_and_wrap/nsg_20_rules", |b| {
b.iter(|| {
registry.normalize_and_wrap(
black_box(&resource),
None,
Some(context.clone()),
Some(parameters.clone()),
)
})
});
}
fn bench_registry_load(c: &mut Criterion) {
c.bench_function("registry/load_from_json", |b| {
b.iter(|| {
let mut reg = AliasRegistry::new();
reg.load_from_json(black_box(ALIASES_JSON)).unwrap();
reg
})
});
}
// ─── Large-payload benchmarks ───────────────────────────────────────────────
//
// These stress the hot paths identified in the performance analysis:
// - Nested set helpers (alias-heavy catalog with deep properties)
// - Array element remap/cleanup/rewrap (large sub-resource arrays)
// - Scalar denormalization lookups (many aliases × many fields)
/// Build a large alias catalog with `n` scalar aliases for storage accounts.
/// Each alias maps to a nested `properties.section_i.field_j` path, creating
/// deep nested-set workloads.
fn large_alias_catalog(n: usize) -> String {
let mut aliases = Vec::new();
for i in 0..n {
let section = i / 10;
let field = i % 10;
aliases.push(format!(
r#"{{
"name": "Microsoft.Storage/storageAccounts/section{section}Field{field}",
"defaultPath": "properties.section{section}.field{field}",
"paths": []
}}"#,
));
}
format!(
r#"[{{
"namespace": "Microsoft.Storage",
"resourceTypes": [{{
"resourceType": "storageAccounts",
"aliases": [{aliases}]
}}]
}}]"#,
aliases = aliases.join(",")
)
}
/// Build a storage account resource whose `properties` contain nested sections
/// matching the large alias catalog.
fn large_storage_resource(alias_count: usize) -> Value {
let mut sections = serde_json::Map::new();
for i in 0..alias_count {
let section = i / 10;
let field = i % 10;
let section_key = format!("section{section}");
let section_obj = sections
.entry(section_key)
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let serde_json::Value::Object(m) = section_obj {
m.insert(format!("field{field}"), serde_json::Value::from(i));
}
}
Value::from(json!({
"name": "bigStorage",
"type": "Microsoft.Storage/storageAccounts",
"location": "westus2",
"properties": sections
}))
}
fn bench_normalize_large_catalog(c: &mut Criterion) {
let mut group = c.benchmark_group("normalize/large_catalog");
for alias_count in [50, 200] {
let catalog_json = large_alias_catalog(alias_count);
let mut reg = AliasRegistry::new();
reg.load_from_json(&catalog_json).unwrap();
let resource = large_storage_resource(alias_count);
group.bench_with_input(
BenchmarkId::from_parameter(alias_count),
&(reg, resource),
|b, (reg, res)| b.iter(|| normalizer::normalize(black_box(res), Some(reg), None)),
);
}
group.finish();
}
fn bench_denormalize_large_catalog(c: &mut Criterion) {
let mut group = c.benchmark_group("denormalize/large_catalog");
for alias_count in [50, 200] {
let catalog_json = large_alias_catalog(alias_count);
let mut reg = AliasRegistry::new();
reg.load_from_json(&catalog_json).unwrap();
let resource = large_storage_resource(alias_count);
let normalized = normalizer::normalize(&resource, Some(&reg), None);
group.bench_with_input(
BenchmarkId::from_parameter(alias_count),
&(reg, normalized),
|b, (reg, norm)| b.iter(|| denormalizer::denormalize(black_box(norm), Some(reg), None)),
);
}
group.finish();
}
fn bench_nsg_large_subarrays(c: &mut Criterion) {
let registry = build_registry();
let mut group = c.benchmark_group("round_trip/nsg_sub_resource");
for rule_count in [50, 200, 500] {
let resource = nsg_resource(rule_count);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&resource,
|b, res| {
b.iter(|| {
let n = normalizer::normalize(black_box(res), Some(&registry), None);
denormalizer::denormalize(&n, Some(&registry), None)
})
},
);
}
group.finish();
}
// ─── Versioned-path benchmarks ──────────────────────────────────────────────
//
// Exercise the precomputed versioned-path aggregates by building a catalog
// where wildcard (array) aliases have version-specific paths that differ from
// the default, then running normalize/denormalize with an explicit api_version.
/// NSG-like alias catalog where wildcard aliases have versioned paths that
/// differ from the default. This forces the normalize/denormalize path through
/// the versioned aggregate lookup rather than the default-aggregate fast path.
const VERSIONED_ALIASES_JSON: &str = r#"[
{
"namespace": "Microsoft.Network",
"resourceTypes": [
{
"resourceType": "networkSecurityGroups",
"aliases": [
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
"defaultPath": "properties.securityRules[*].properties.protocol",
"paths": [
{ "path": "properties.securityRules[*].properties.transportProtocol", "apiVersions": ["2020-01-01"] },
{ "path": "properties.securityRules[*].properties.protocol", "apiVersions": ["2022-01-01"] }
]
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
"defaultPath": "properties.securityRules[*].properties.access",
"paths": [
{ "path": "properties.securityRules[*].properties.accessLevel", "apiVersions": ["2020-01-01"] },
{ "path": "properties.securityRules[*].properties.access", "apiVersions": ["2022-01-01"] }
]
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority",
"defaultPath": "properties.securityRules[*].properties.priority",
"paths": [
{ "path": "properties.securityRules[*].properties.rulePriority", "apiVersions": ["2020-01-01"] },
{ "path": "properties.securityRules[*].properties.priority", "apiVersions": ["2022-01-01"] }
]
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction",
"defaultPath": "properties.securityRules[*].properties.direction",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix",
"defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange",
"defaultPath": "properties.securityRules[*].properties.destinationPortRange",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
"defaultPath": "properties.securityRules[*].name",
"paths": []
},
{
"name": "Microsoft.Network/networkSecurityGroups/provisioningState",
"defaultPath": "properties.provisioningState",
"paths": [
{ "path": "properties.state", "apiVersions": ["2020-01-01"] },
{ "path": "properties.provisioningState", "apiVersions": ["2022-01-01"] }
]
}
]
}
]
}
]"#;
fn build_versioned_registry() -> AliasRegistry {
let mut reg = AliasRegistry::new();
reg.load_from_json(VERSIONED_ALIASES_JSON).unwrap();
reg
}
/// Build an NSG resource for versioned-path benchmarks.
/// Uses the 2020-01-01 field names (`transportProtocol`, `accessLevel`,
/// `rulePriority`) so that versioned path resolution actually differs from
/// the default.
fn nsg_versioned_resource(rule_count: usize) -> Value {
let rules: Vec<serde_json::Value> = (0..rule_count)
.map(|i| {
json!({
"name": format!("rule-{}", i),
"properties": {
"transportProtocol": "Tcp",
"accessLevel": if i % 2 == 0 { "Allow" } else { "Deny" },
"rulePriority": 100 + i,
"direction": "Inbound",
"sourceAddressPrefix": format!("10.0.{}.0/24", i % 256),
"destinationPortRange": format!("{}", 80 + i)
}
})
})
.collect();
to_regorus(json!({
"name": "myNsg",
"type": "Microsoft.Network/networkSecurityGroups",
"location": "eastus",
"properties": {
"state": "Succeeded",
"securityRules": rules
}
}))
}
fn bench_normalize_versioned(c: &mut Criterion) {
let registry = build_versioned_registry();
let mut group = c.benchmark_group("normalize_versioned/nsg_rules");
for rule_count in [5, 20, 100] {
let resource = nsg_versioned_resource(rule_count);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&resource,
|b, res| {
b.iter(|| {
normalizer::normalize(black_box(res), Some(&registry), Some("2020-01-01"))
})
},
);
}
group.finish();
}
fn bench_denormalize_versioned(c: &mut Criterion) {
let registry = build_versioned_registry();
let mut group = c.benchmark_group("denormalize_versioned/nsg_rules");
for rule_count in [5, 20, 100] {
let resource = nsg_versioned_resource(rule_count);
let normalized = normalizer::normalize(&resource, Some(&registry), Some("2020-01-01"));
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&normalized,
|b, norm| {
b.iter(|| {
denormalizer::denormalize(black_box(norm), Some(&registry), Some("2020-01-01"))
})
},
);
}
group.finish();
}
fn bench_round_trip_versioned(c: &mut Criterion) {
let registry = build_versioned_registry();
let mut group = c.benchmark_group("round_trip_versioned/nsg_rules");
for rule_count in [20, 100] {
let resource = nsg_versioned_resource(rule_count);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&resource,
|b, res| {
b.iter(|| {
let n =
normalizer::normalize(black_box(res), Some(&registry), Some("2020-01-01"));
denormalizer::denormalize(&n, Some(&registry), Some("2020-01-01"))
})
},
);
}
group.finish();
}
criterion_group!(
normalization_benches,
bench_normalize_simple,
bench_normalize_no_aliases,
bench_normalize_nsg_scaling,
bench_denormalize_simple,
bench_denormalize_nsg_scaling,
bench_round_trip,
bench_normalize_and_wrap,
bench_registry_load,
bench_normalize_large_catalog,
bench_denormalize_large_catalog,
bench_nsg_large_subarrays,
bench_normalize_versioned,
bench_denormalize_versioned,
bench_round_trip_versioned,
);
criterion_main!(normalization_benches);

680
benches/rvm_benchmark.rs Normal file
View File

@@ -0,0 +1,680 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Comprehensive RVM benchmarks covering all aspects of the Rego Virtual Machine.
//!
//! # Policy families
//!
//! | Family | Source | Policies | Inputs/policy |
//! |------------|-------------------------------|----------|---------------|
//! | Synthetic | `benches/evaluation/test_data`| 9 | 3 each |
//! | ACI | `tests/aci` | 9 | 1 each |
//!
//! # Benchmark groups
//!
//! | Group | What it measures |
//! |--------------------------|-------------------------------------------------------|
//! | `cold/{case}/{config}` | Cold: new VM + load + data + input + execute |
//! | `hot/{case}/{config}` | Hot: set_input + execute (VM reused across iters) |
//! | `compilation` | Rego CompiledPolicy → RVM Program |
//! | `serialization` | Program binary serialize / deserialize roundtrip |
//! | `startup` | Isolated VM creation & setup overhead |
//! | `stats` | Instruction/literal counts (reported as throughput) |
//! | `end_to_end` | Full roundtrip: compile → serialize → deserialize → eval |
//!
//! # Running subsets
//!
//! ```sh
//! cargo bench --bench rvm_benchmark # everything
//! cargo bench --bench rvm_benchmark -- cold # all cold eval
//! cargo bench --bench rvm_benchmark -- hot # all hot eval
//! cargo bench --bench rvm_benchmark -- regular_with_limits # one config across cases
//! cargo bench --bench rvm_benchmark -- cold/aci/ # all ACI cold benchmarks
//! cargo bench --bench rvm_benchmark -- rbac # one policy family
//! cargo bench --bench rvm_benchmark -- compilation # compilation only
//! cargo bench --bench rvm_benchmark -- serialization # serialization only
//! cargo bench --bench rvm_benchmark -- startup # startup overhead
//! ```
use std::hint::black_box;
use std::num::NonZeroU32;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::Program;
use regorus::rvm::vm::{ExecutionMode, RegoVM};
use regorus::utils::limits::ExecutionTimerConfig;
use regorus::{Engine, Rc, Value};
// ---------------------------------------------------------------------------
// Limit constants generous ceilings that still exercise the limit-checking
// hot path (memory_check, execution_timer_tick, instruction-limit compare).
// ---------------------------------------------------------------------------
#[cfg(feature = "allocator-memory-limits")]
const MEMORY_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
const TIME_LIMIT: Duration = Duration::from_secs(30);
const TIMER_CHECK_INTERVAL: NonZeroU32 = NonZeroU32::new(16).unwrap();
const INSTRUCTION_LIMIT: usize = 10_000_000;
#[derive(Clone, Copy)]
struct EvalConfig {
name: &'static str,
mode: ExecutionMode,
limits: bool,
}
const EVAL_CONFIGS: [EvalConfig; 4] = [
EvalConfig {
name: "regular_no_limits",
mode: ExecutionMode::RunToCompletion,
limits: false,
},
EvalConfig {
name: "regular_with_limits",
mode: ExecutionMode::RunToCompletion,
limits: true,
},
EvalConfig {
name: "suspendable_no_limits",
mode: ExecutionMode::Suspendable,
limits: false,
},
EvalConfig {
name: "suspendable_with_limits",
mode: ExecutionMode::Suspendable,
limits: true,
},
];
// ---------------------------------------------------------------------------
// Data types
// ---------------------------------------------------------------------------
/// A compiled benchmark program ready for RVM execution.
struct BenchmarkProgram {
/// Human-readable name (e.g. "rbac_policy" or "aci/create_container").
name: String,
/// Pre-compiled RVM program.
program: Arc<Program>,
/// Compiled policy (kept for compilation benchmarks).
compiled_policy: regorus::CompiledPolicy,
/// Entry-point rule path.
entry_point: String,
/// Data object (Some for policies that require external data like ACI).
data: Option<Value>,
/// Named inputs for this policy.
inputs: Vec<(String, Value)>,
}
// ---------------------------------------------------------------------------
// ACI YAML types
// ---------------------------------------------------------------------------
#[derive(Serialize, Deserialize, Debug)]
struct AciTestCase {
note: String,
data: Value,
input: Value,
modules: Vec<String>,
query: String,
want_result: Value,
}
#[derive(Serialize, Deserialize, Debug)]
struct AciYamlTest {
cases: Vec<AciTestCase>,
}
// ---------------------------------------------------------------------------
// Synthetic policy loading
// ---------------------------------------------------------------------------
/// Policy ↔ input file mapping for synthetic policies.
const SYNTHETIC_POLICIES: &[(&str, &str, &[&str])] = &[
(
"rbac_policy",
"rbac_policy.rego",
&["rbac_input.json", "rbac_input2.json", "rbac_input3.json"],
),
(
"api_access",
"api_access_policy.rego",
&[
"api_access_input.json",
"api_access_input2.json",
"api_access_input3.json",
],
),
(
"data_sensitivity",
"data_sensitivity_policy.rego",
&[
"data_sensitivity_input.json",
"data_sensitivity_input2.json",
"data_sensitivity_input3.json",
],
),
(
"time_based",
"time_based_policy.rego",
&[
"time_based_input.json",
"time_based_input2.json",
"time_based_input3.json",
],
),
(
"data_processing",
"data_processing_policy.rego",
&[
"data_processing_input.json",
"data_processing_input2.json",
"data_processing_input3.json",
],
),
(
"azure_vm",
"azure_vm_policy.rego",
&[
"azure_vm_input.json",
"azure_vm_input2.json",
"azure_vm_input3.json",
],
),
(
"azure_storage",
"azure_storage_policy.rego",
&[
"azure_storage_input.json",
"azure_storage_input2.json",
"azure_storage_input3.json",
],
),
(
"azure_keyvault",
"azure_keyvault_policy.rego",
&[
"azure_keyvault_input.json",
"azure_keyvault_input2.json",
"azure_keyvault_input3.json",
],
),
(
"azure_nsg",
"azure_nsg_policy.rego",
&[
"azure_nsg_input.json",
"azure_nsg_input2.json",
"azure_nsg_input3.json",
],
),
];
/// Compile synthetic Rego policies into RVM programs.
fn compile_synthetic_programs() -> Vec<BenchmarkProgram> {
let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("benches")
.join("evaluation")
.join("test_data");
let entry_point = "data.bench.allow";
let entry_point_rc: Rc<str> = entry_point.into();
SYNTHETIC_POLICIES
.iter()
.map(|(name, policy_file, input_files)| {
let policy_path = base_dir.join("policies").join(policy_file);
let policy_content = std::fs::read_to_string(&policy_path)
.unwrap_or_else(|e| panic!("Failed to read {policy_path:?}: {e}"));
let mut engine = Engine::new();
engine
.add_policy("policy.rego".to_string(), policy_content)
.expect("failed to add policy");
let compiled_policy = engine
.compile_with_entrypoint(&entry_point_rc)
.expect("failed to compile policy");
let program = Compiler::compile_from_policy(&compiled_policy, &[entry_point])
.expect("failed to compile to RVM program");
let inputs: Vec<(String, Value)> = input_files
.iter()
.map(|input_file| {
let input_path = base_dir.join("inputs").join(input_file);
let json = std::fs::read_to_string(&input_path)
.unwrap_or_else(|e| panic!("Failed to read {input_path:?}: {e}"));
let value = Value::from_json_str(&json).expect("failed to parse input JSON");
let display = input_file.trim_end_matches(".json").to_string();
(display, value)
})
.collect();
BenchmarkProgram {
name: name.to_string(),
program,
compiled_policy,
entry_point: entry_point.to_string(),
data: None,
inputs,
}
})
.collect()
}
// ---------------------------------------------------------------------------
// ACI policy loading
// ---------------------------------------------------------------------------
/// Load all ACI test cases from YAML files.
fn load_aci_cases(dir: &Path) -> Vec<AciTestCase> {
let mut cases = Vec::new();
for entry in WalkDir::new(dir)
.sort_by_file_name()
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.to_string_lossy().ends_with(".yaml") {
continue;
}
let yaml = std::fs::read(path).expect("failed to read yaml");
let yaml = String::from_utf8_lossy(&yaml);
let test: AciYamlTest = serde_yaml::from_str(&yaml).expect("failed to deserialize yaml");
cases.extend(test.cases);
}
cases
}
/// Build an Engine with policies loaded for a given ACI test case.
fn build_aci_engine(dir: &Path, case: &AciTestCase) -> Engine {
let mut engine = Engine::new();
engine.set_rego_v0(true);
engine
.add_data(case.data.clone())
.expect("failed to add data");
engine.set_input(case.input.clone());
for (idx, rego) in case.modules.iter().enumerate() {
if rego.ends_with(".rego") {
engine
.add_policy_from_file(dir.join(rego).to_str().expect("invalid path"))
.expect("failed to add policy");
} else {
engine
.add_policy(format!("rego{idx}.rego"), rego.clone())
.expect("failed to add policy");
}
}
engine
}
/// Compile ACI test cases into RVM programs.
fn compile_aci_programs() -> Vec<BenchmarkProgram> {
let dir = Path::new("tests/aci");
load_aci_cases(dir)
.into_iter()
.map(|case| {
let mut engine = build_aci_engine(dir, &case);
let rule = case.query.replace("=x", "");
let rule_rc: Rc<str> = rule.clone().into();
let compiled_policy = engine
.compile_with_entrypoint(&rule_rc)
.expect("failed to compile");
let program = Compiler::compile_from_policy(&compiled_policy, &[rule.as_str()])
.expect("failed to compile to RVM");
BenchmarkProgram {
name: format!("aci/{}", case.note),
program,
compiled_policy,
entry_point: rule,
data: Some(case.data),
inputs: vec![("input".to_string(), case.input)],
}
})
.collect()
}
// ---------------------------------------------------------------------------
// Compile all policies
// ---------------------------------------------------------------------------
/// Compile all policies (synthetic + ACI) into RVM programs.
fn compile_all_programs() -> Vec<BenchmarkProgram> {
let mut programs = compile_synthetic_programs();
programs.extend(compile_aci_programs());
programs
}
// ---------------------------------------------------------------------------
// Limit helpers
// ---------------------------------------------------------------------------
/// Apply or remove production-style limits based on a boolean flag.
fn configure_limits(vm: &mut RegoVM, limits: bool) {
if limits {
#[cfg(feature = "allocator-memory-limits")]
regorus::set_global_memory_limit(Some(MEMORY_LIMIT_BYTES));
vm.set_execution_timer_config(Some(ExecutionTimerConfig {
limit: TIME_LIMIT,
check_interval: TIMER_CHECK_INTERVAL,
}));
vm.set_max_instructions(INSTRUCTION_LIMIT);
} else {
#[cfg(feature = "allocator-memory-limits")]
regorus::set_global_memory_limit(None);
vm.set_execution_timer_config(None);
vm.set_max_instructions(usize::MAX);
}
}
// ---------------------------------------------------------------------------
// Cold evaluation — new VM per iteration (full setup + execute)
//
// Benchmarks are registered case-first so each workload is shown with all
// config variants adjacent to one another, making per-case comparisons easier.
// ---------------------------------------------------------------------------
fn bench_cold(c: &mut Criterion) {
let programs = compile_all_programs();
let mut group = c.benchmark_group("cold");
for bp in &programs {
for (input_name, input_value) in &bp.inputs {
let case_id = if bp.inputs.len() == 1 {
bp.name.clone()
} else {
format!("{}/{}", bp.name, input_name)
};
let program = bp.program.clone();
let data = bp.data.clone();
let input = input_value.clone();
for config in EVAL_CONFIGS {
group.bench_function(BenchmarkId::new(&case_id, config.name), |b| {
b.iter(|| {
let mut vm = RegoVM::new();
vm.set_execution_mode(config.mode);
vm.load_program(black_box(program.clone()));
if let Some(ref d) = data {
vm.set_data(black_box(d.clone())).unwrap();
}
vm.set_input(black_box(input.clone()));
configure_limits(&mut vm, config.limits);
black_box(vm.execute().unwrap())
})
});
}
}
}
group.finish();
}
// ---------------------------------------------------------------------------
// Hot evaluation — VM reused across iterations
//
// The VM is created once with program, data, mode, and limits. Each
// iteration only calls set_input + execute, measuring pure execution
// overhead with minimal setup. A warm-up execution fills the register
// window pool so all iterations benefit from pooled allocations.
// ---------------------------------------------------------------------------
fn bench_hot(c: &mut Criterion) {
let programs = compile_all_programs();
let mut group = c.benchmark_group("hot");
for bp in &programs {
let program = bp.program.clone();
let data = bp.data.clone();
let inputs: Vec<Value> = bp.inputs.iter().map(|(_, v)| v.clone()).collect();
let num_inputs = inputs.len();
for config in EVAL_CONFIGS {
group.bench_function(BenchmarkId::new(&bp.name, config.name), |b| {
let mut vm = RegoVM::new();
vm.set_execution_mode(config.mode);
vm.load_program(program.clone());
if let Some(ref d) = data {
vm.set_data(d.clone()).unwrap();
}
configure_limits(&mut vm, config.limits);
// Warm up: fill register window pools, caches, etc.
vm.set_input(inputs[0].clone());
vm.execute().expect("warm-up failed");
let mut i = 0usize;
b.iter(|| {
let input = &inputs[i % num_inputs];
vm.set_input(black_box(input.clone()));
black_box(vm.execute().unwrap());
i += 1;
})
});
}
}
group.finish();
}
// ---------------------------------------------------------------------------
// Compilation — Rego CompiledPolicy → RVM Program
// ---------------------------------------------------------------------------
fn bench_compilation(c: &mut Criterion) {
let programs = compile_all_programs();
let mut group = c.benchmark_group("compilation");
for bp in &programs {
let entry_point: &str = &bp.entry_point;
group.bench_with_input(
BenchmarkId::new("rego_to_rvm", &bp.name),
&bp.compiled_policy,
|b, compiled_policy| {
b.iter(|| {
Compiler::compile_from_policy(
black_box(compiled_policy),
black_box(&[entry_point]),
)
.unwrap();
})
},
);
}
group.finish();
}
// ---------------------------------------------------------------------------
// Serialization — binary serialize / deserialize roundtrip
// ---------------------------------------------------------------------------
fn bench_serialization(c: &mut Criterion) {
let programs = compile_all_programs();
let mut group = c.benchmark_group("serialization");
for bp in &programs {
let program = &bp.program;
let serialized = program
.serialize_binary()
.expect("failed to serialize program");
let byte_len = serialized.len() as u64;
group.throughput(Throughput::Bytes(byte_len));
group.bench_function(BenchmarkId::new("serialize", &bp.name), |b| {
b.iter(|| black_box(program.serialize_binary().unwrap()))
});
group.throughput(Throughput::Bytes(byte_len));
group.bench_function(BenchmarkId::new("deserialize", &bp.name), |b| {
b.iter(|| black_box(Program::deserialize_binary(black_box(&serialized)).unwrap()))
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Startup — isolated VM creation & setup overhead
// ---------------------------------------------------------------------------
fn bench_startup(c: &mut Criterion) {
let programs = compile_all_programs();
let mut group = c.benchmark_group("startup");
// Use the first program as representative for startup overhead.
let bp = &programs[0];
let program = bp.program.clone();
let input = bp.inputs[0].1.clone();
// Bare VM creation
group.bench_function("new", |b| b.iter(|| black_box(RegoVM::new())));
// load_program (Arc clone + internal setup)
group.bench_function("load_program", |b| {
b.iter(|| {
let mut vm = RegoVM::new();
vm.load_program(black_box(program.clone()));
black_box(&vm);
})
});
// set_input
group.bench_function("set_input", |b| {
let mut vm = RegoVM::new();
vm.load_program(program.clone());
b.iter(|| {
vm.set_input(black_box(input.clone()));
})
});
group.finish();
}
// ---------------------------------------------------------------------------
// Stats — instruction / literal counts (reported as throughput)
// ---------------------------------------------------------------------------
fn bench_stats(c: &mut Criterion) {
let programs = compile_all_programs();
eprintln!();
eprintln!(
"{:<30} {:>8} {:>8} {:>8} {:>10}",
"program", "instrs", "lits", "entries", "bytes"
);
eprintln!("{}", "-".repeat(70));
let mut group = c.benchmark_group("stats");
for bp in &programs {
let serialized = bp.program.serialize_binary().expect("serialize failed");
let byte_len = serialized.len();
let instr_count = bp.program.instructions.len();
let lit_count = bp.program.literals.len();
let entry_count = bp.program.entry_points.len();
eprintln!(
"{:<30} {:>8} {:>8} {:>8} {:>10}",
bp.name, instr_count, lit_count, entry_count, byte_len,
);
group.throughput(Throughput::Elements(instr_count as u64));
group.bench_function(BenchmarkId::new("serialize", &bp.name), |b| {
b.iter(|| black_box(bp.program.serialize_binary().unwrap()))
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// End-to-end roundtrip (compile + serialize + deserialize + eval)
//
// Only runs for synthetic policies where we have direct access to rego
// source files. ACI policies are loaded from YAML with module references
// which makes the setup pipeline different.
// ---------------------------------------------------------------------------
fn bench_end_to_end(c: &mut Criterion) {
let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("benches")
.join("evaluation")
.join("test_data");
let entry_point = "data.bench.allow";
let entry_point_rc: Rc<str> = entry_point.into();
let mut group = c.benchmark_group("end_to_end");
for &(name, policy_file, input_files) in SYNTHETIC_POLICIES {
let policy_path = base_dir.join("policies").join(policy_file);
let policy_content = std::fs::read_to_string(&policy_path)
.unwrap_or_else(|e| panic!("Failed to read {policy_path:?}: {e}"));
// Use just the first input for end-to-end
let input_path = base_dir.join("inputs").join(input_files[0]);
let input_json = std::fs::read_to_string(&input_path)
.unwrap_or_else(|e| panic!("Failed to read {input_path:?}: {e}"));
group.bench_function(BenchmarkId::new("roundtrip", name), |b| {
b.iter(|| {
// 1. Engine + parse
let mut engine = Engine::new();
engine
.add_policy("policy.rego".to_string(), policy_content.clone())
.unwrap();
// 2. Compile to CompiledPolicy
let compiled_policy = engine.compile_with_entrypoint(&entry_point_rc).unwrap();
// 3. Compile to RVM Program
let program =
Compiler::compile_from_policy(&compiled_policy, &[entry_point]).unwrap();
// 4. Serialize
let bytes = program.serialize_binary().unwrap();
// 5. Deserialize
let deserialized = Program::deserialize_binary(&bytes).unwrap();
let program = match deserialized {
regorus::rvm::program::DeserializationResult::Complete(p) => Arc::new(p),
regorus::rvm::program::DeserializationResult::Partial(p) => {
Arc::new(Program::compile_from_partial(p).unwrap())
}
};
// 6. Execute
let mut vm = RegoVM::new();
vm.load_program(program);
let input = Value::from_json_str(&input_json).unwrap();
vm.set_input(input);
black_box(vm.execute().unwrap());
})
});
}
group.finish();
}
// ---------------------------------------------------------------------------
// Criterion groups — organised for selective runs
// ---------------------------------------------------------------------------
criterion_group!(cold_benches, bench_cold);
criterion_group!(hot_benches, bench_hot);
criterion_group!(
misc_benches,
bench_compilation,
bench_serialization,
bench_startup,
bench_stats,
bench_end_to_end,
);
criterion_main!(cold_benches, hot_benches, misc_benches);

View File

@@ -13,6 +13,7 @@ FetchContent_Declare(
FetchContent_MakeAvailable(Corrosion)
project("regorus-test")
enable_testing()
corrosion_import_crate(
# Path to <regorus-source-folder>/bindings/ffi/Cargo.toml
@@ -35,3 +36,10 @@ add_executable(regorus_test main.c)
# Add path to <regorus-source-folder>/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)

View File

@@ -11,6 +11,20 @@ int main() {
if (r.status != Ok)
goto error;
// Configure the global pattern caches.
RegorusCacheConfig cache_config = { .regex = 256, .glob = 128 };
r = regorus_set_cache_config(cache_config);
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Raise the default col limit to 2000
RegorusPolicyLengthConfig len_config = { .max_col = 2000, .max_file_bytes = 1048576, .max_lines = 20000 };
r = regorus_engine_set_policy_length_config(engine, len_config);
if (r.status != Ok)
goto error;
regorus_result_drop(r);
// Load policies.
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/framework.rego");
if (r.status != Ok)

289
bindings/c/rvm_tests.c Normal file
View File

@@ -0,0 +1,289 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include <stdio.h>
#include <string.h>
#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;
}

View File

@@ -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)

View File

@@ -6,8 +6,19 @@ void example()
// Create engine
regorus::Engine engine;
// Configure the global pattern caches.
RegorusCacheConfig cache_config = { 256, 128 };
regorus::set_cache_config(cache_config);
engine.set_rego_v0(true);
engine.set_enable_coverage(true);
RegorusPolicyLengthConfig len_config;
// Raise the default col limit to 2000
len_config.max_col = 2000;
len_config.max_file_bytes = 1048576;
len_config.max_lines = 20000;
engine.set_policy_length_config(len_config);
// Add policies.
engine.add_policy("objects.rego",R"(package objects

View File

@@ -1,6 +1,8 @@
#ifndef REGORUS_WRAPPER_HPP
#define REGORUS_WRAPPER_HPP
#include <cstddef>
#include <cstdint>
#include <memory>
#include <variant>
@@ -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;
};
@@ -105,10 +131,22 @@ namespace regorus {
Result get_coverage_report_pretty() {
return Result(regorus_engine_get_coverage_report_pretty(engine));
}
Result set_policy_length_config(RegorusPolicyLengthConfig config) {
return Result(regorus_engine_set_policy_length_config(engine, config));
}
Result clear_policy_length_config() {
return Result(regorus_engine_clear_policy_length_config(engine));
}
~Engine() {
regorus_engine_drop(engine);
}
RegorusEngine* raw() const {
return engine;
}
private:
@@ -119,6 +157,255 @@ namespace regorus {
Engine(Engine&&) = delete;
Engine& operator=(const Engine&) = delete;
};
inline Result set_cache_config(RegorusCacheConfig config) {
return Result(regorus_set_cache_config(config));
}
inline Result clear_cache() {
return Result(regorus_clear_cache());
}
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<RegorusProgram*>(result.pointer_value));
}
inline Buffer Result::buffer() const {
return Buffer(reinterpret_cast<RegorusBuffer*>(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

261
bindings/cpp/rvm_tests.cpp Normal file
View File

@@ -0,0 +1,261 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include <iostream>
#include <string>
#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<RegorusBuffer*>(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;
}

View File

@@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)"/>
<PackageReference Include="Regorus" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,7 +12,7 @@ namespace Benchmarks
public class CompiledPolicyEvaluationBenchmark
{
private static readonly string TestDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
Directory.GetCurrentDirectory(),
"..", "..", "..",
"benches", "evaluation", "test_data"
);
@@ -33,7 +33,7 @@ namespace Benchmarks
private static readonly string[] PolicyNames = new[]
{
"rbac_policy",
"api_access_policy",
"api_access_policy",
"data_sensitivity_policy",
"time_based_policy",
"data_processing_policy",
@@ -46,21 +46,21 @@ namespace Benchmarks
private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs()
{
var result = new List<(string Policy, string[] Inputs)>();
foreach (var (policyFile, inputFiles) in PolicyInputFiles)
{
var policyPath = Path.Combine(TestDataPath, "policies", policyFile);
var policy = File.ReadAllText(policyPath);
var inputs = inputFiles.Select(inputFile =>
{
var inputPath = Path.Combine(TestDataPath, "inputs", inputFile);
return File.ReadAllText(inputPath);
}).ToArray();
result.Add((policy, inputs));
}
return result;
}
@@ -68,14 +68,14 @@ namespace Benchmarks
{
var policiesWithInputs = LoadPoliciesWithInputs();
var compiledPolicies = new List<CompiledPolicy>();
foreach (var (policy, _) in policiesWithInputs)
{
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
compiledPolicies.Add(compiled);
}
return compiledPolicies;
}
@@ -84,13 +84,13 @@ namespace Benchmarks
var cpuCount = Environment.ProcessorCount;
var maxThreads = cpuCount * 2;
var threadCounts = new List<int> { 1, 2 };
// Add even numbers from 4 to maxThreads
for (int i = 4; i <= maxThreads; i += 2)
{
threadCounts.Add(i);
}
Console.WriteLine($"Running compiled policy benchmark with max_threads: {maxThreads}");
Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}");
Console.WriteLine();
@@ -120,19 +120,19 @@ namespace Benchmarks
const int durationSeconds = 3;
var policiesWithInputs = LoadPoliciesWithInputs();
List<CompiledPolicy>? compiledPolicies = null;
if (useSharedPolicies)
{
compiledPolicies = PrepareSharedCompiledPolicies();
}
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase
var (_, _, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters, allocatedBytes) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false);
@@ -155,7 +155,7 @@ namespace Benchmarks
{
foreach (var policy in compiledPolicies)
{
policy.Dispose();
DisposeCompiledPolicy(policy);
}
}
@@ -173,8 +173,8 @@ namespace Benchmarks
}
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters, long allocatedBytes) RunBenchmarkPhase(
int threads,
int durationSeconds,
int threads,
int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs,
List<CompiledPolicy>? compiledPolicies,
bool useSharedPolicies,
@@ -208,10 +208,10 @@ namespace Benchmarks
}
barrier.SignalAndWait();
int evaluationCount = 0;
var localEvaluationTime = TimeSpan.Zero;
while (!stopExecution)
{
// Use different policy for each iteration
@@ -226,23 +226,29 @@ namespace Benchmarks
{
// Measure only the evaluation call
var evalStopwatch = Stopwatch.StartNew();
if (useSharedPolicies)
{
var result = compiledPolicies![policyIdx].EvalWithInput(input);
}
else
{
// Compile policy in each iteration
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
// Compile policy in each iteration.
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
var result = compiled.EvalWithInput(input);
compiled.Dispose();
try
{
var result = compiled.EvalWithInput(input);
}
finally
{
DisposeCompiledPolicy(compiled);
}
}
evalStopwatch.Stop();
localEvaluationTime += evalStopwatch.Elapsed;
// Track successful evaluations (only during actual benchmark, not warmup)
if (!isWarmup)
{
@@ -256,10 +262,10 @@ namespace Benchmarks
{
// Ignore evaluation errors for benchmarking purposes
}
evaluationCount++;
}
// Store the actual evaluation time for this thread
if (!isWarmup)
{
@@ -284,11 +290,23 @@ namespace Benchmarks
var totalEvaluations = policyCounters.Values.Sum();
var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time);
// Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes);
}
private static void DisposeCompiledPolicy(CompiledPolicy policy)
{
try
{
policy.Dispose();
}
catch (TimeoutException ex)
{
Console.WriteLine($"Warning: {ex.Message}");
}
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Benchmarks
public class EngineEvaluationBenchmark
{
private static readonly string TestDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
Directory.GetCurrentDirectory(),
"..", "..", "..",
"benches", "evaluation", "test_data"
);
@@ -33,7 +33,7 @@ namespace Benchmarks
private static readonly string[] PolicyNames = new[]
{
"rbac_policy",
"api_access_policy",
"api_access_policy",
"data_sensitivity_policy",
"time_based_policy",
"data_processing_policy",
@@ -46,21 +46,21 @@ namespace Benchmarks
private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs()
{
var result = new List<(string Policy, string[] Inputs)>();
foreach (var (policyFile, inputFiles) in PolicyInputFiles)
{
var policyPath = Path.Combine(TestDataPath, "policies", policyFile);
var policy = File.ReadAllText(policyPath);
var inputs = inputFiles.Select(inputFile =>
{
var inputPath = Path.Combine(TestDataPath, "inputs", inputFile);
return File.ReadAllText(inputPath);
}).ToArray();
result.Add((policy, inputs));
}
return result;
}
@@ -68,12 +68,12 @@ namespace Benchmarks
{
var policiesWithInputs = LoadPoliciesWithInputs();
var engines = new List<Engine>();
foreach (var (policy, _) in policiesWithInputs)
{
var engine = new Engine();
engine.AddPolicy("policy.rego", policy);
// Warm up the engine to ensure it's fully prepared for evaluation
// This prevents each cloned engine from repeating preparation work
engine.SetInputJson("{}");
@@ -85,10 +85,10 @@ namespace Benchmarks
{
// Ignore warmup errors
}
engines.Add(engine);
}
return engines;
}
@@ -97,13 +97,13 @@ namespace Benchmarks
var cpuCount = Environment.ProcessorCount;
var maxThreads = cpuCount * 2;
var threadCounts = new List<int> { 1, 2 };
// Add even numbers from 4 to maxThreads
for (int i = 4; i <= maxThreads; i += 2)
{
threadCounts.Add(i);
}
Console.WriteLine($"Running engine benchmark with max_threads: {maxThreads}");
Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}");
Console.WriteLine();
@@ -132,14 +132,14 @@ namespace Benchmarks
const int warmupSeconds = 3;
const int durationSeconds = 3;
var policiesWithInputs = LoadPoliciesWithInputs();
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase
var (_, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, useClonedEngines, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, useClonedEngines, isWarmup: false);
@@ -165,8 +165,8 @@ namespace Benchmarks
}
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters) RunBenchmarkPhase(
int threads,
int durationSeconds,
int threads,
int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs,
bool useClonedEngines,
bool isWarmup)
@@ -199,10 +199,10 @@ namespace Benchmarks
tasks[threadId] = Task.Run(() =>
{
barrier.SignalAndWait();
int evaluationCount = 0;
var localEvaluationTime = TimeSpan.Zero;
while (!stopExecution)
{
// Use different policy for each iteration
@@ -217,7 +217,7 @@ namespace Benchmarks
{
// Measure only the engine operations
var evalStopwatch = Stopwatch.StartNew();
Engine engine;
if (useClonedEngines)
{
@@ -228,14 +228,14 @@ namespace Benchmarks
engine = new Engine();
engine.AddPolicy("policy.rego", policy);
}
engine.SetInputJson(input);
var result = engine.EvalRule("data.bench.allow");
engine.Dispose();
evalStopwatch.Stop();
localEvaluationTime += evalStopwatch.Elapsed;
// Track successful evaluations (only during actual benchmark, not warmup)
if (!isWarmup)
{
@@ -249,10 +249,10 @@ namespace Benchmarks
{
// Ignore evaluation errors for benchmarking purposes
}
evaluationCount++;
}
// Store the actual evaluation time for this thread
if (!isWarmup)
{
@@ -283,10 +283,10 @@ namespace Benchmarks
var totalEvaluations = policyCounters.Values.Sum();
var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time);
// Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters);
}
}

View File

@@ -7,7 +7,7 @@ namespace Benchmarks
static void Main(string[] args)
{
Console.WriteLine("=== Regorus C# Benchmarks ===\n");
try
{
Console.WriteLine("Running Engine Evaluation Benchmark...");
@@ -17,9 +17,9 @@ namespace Benchmarks
{
Console.WriteLine($"Engine benchmark failed: {ex.Message}");
}
Console.WriteLine("\n" + new string('=', 80) + "\n");
try
{
Console.WriteLine("Running Compiled Policy Evaluation Benchmark...");
@@ -29,7 +29,7 @@ namespace Benchmarks
{
Console.WriteLine($"Compiled policy benchmark failed: {ex.Message}");
}
Console.WriteLine("\n=== Benchmarks Complete ===");
}
}

View File

@@ -0,0 +1,15 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RegorusPackageVersion>0.9.1</RegorusPackageVersion>
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup>
<ItemGroup>
<!-- Centralize Regorus package version with optional CI suffix -->
<PackageVersion Include="Regorus" Version="$(RegorusPackageVersion)$(RegorusPackageVersionSuffix)" />
<PackageVersion Include="MSTest" Version="3.8.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="YamlDotNet" Version="13.7.0" />
</ItemGroup>
</Project>

View File

@@ -29,4 +29,124 @@ Once the workflow run completes, the generated Nuget can be downloaded by follow
## Local
TODO
The `cargo xtask` runner provides helpers for local builds:
1. `cargo xtask ffi` builds the `bindings/ffi` crate for the host platform in debug mode. Add `--target <triple>` (repeatable) to cross-compile, or `--release` to produce optimised artefacts. Results land under `bindings/ffi/target/<triple>/<profile>`.
2. `cargo xtask nuget` reuses those artefacts to pack the C# library. It defaults to debug builds for the host but accepts `--target`, `--release`, `--artifacts-dir <path>` to reuse existing binaries, and `--enforce-artifacts` to require every officially supported platform.
3. `cargo xtask test-csharp` ensures a NuGet is available (rebuilding when required or when `--force-nuget` is passed) and then runs `Regorus.Tests`, `TestApp`, and `TargetExampleApp` against it. The command accepts the same build flags as `cargo xtask nuget`.
## Memory Usage Safeguards
The C# bindings expose allocator-backed memory tracking utilities via the static `Regorus.MemoryLimits` helper. Typical usage:
```csharp
// Restrict total allocations to 128 MiB for the process
Regorus.MemoryLimits.SetGlobalMemoryLimit(128 * 1024 * 1024);
// Optional: tune how frequently each thread flushes its allocation counters
Regorus.MemoryLimits.SetThreadFlushThresholdOverride(256 * 1024);
// Engine operations throw InvalidOperationException with the allocator message if the budget is exceeded
using var engine = new Regorus.Engine();
var veryLargeJson = new string('x', 128 * 1024);
try
{
engine.SetInputJson(veryLargeJson);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Allocator reported: {ex.Message}");
}
// Restore defaults once done
Regorus.MemoryLimits.SetGlobalMemoryLimit(null);
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}");
```
## Azure RBAC Condition Evaluation
Evaluate Azure RBAC condition expressions directly with a JSON evaluation context:
```csharp
using Regorus;
const string Condition = "@Resource[owner] StringEquals 'alice'";
const string ContextJson = """
{
"principal": {
"id": "user-1",
"principal_type": "User",
"custom_security_attributes": {}
},
"resource": {
"id": "/subscriptions/s1",
"resource_type": "Microsoft.Storage/storageAccounts",
"scope": "/subscriptions/s1",
"attributes": {
"owner": "alice",
"confidential": true
}
},
"request": {
"action": "Microsoft.Storage/storageAccounts/read",
"data_action": null,
"attributes": {
"clientIP": "10.0.0.1"
}
},
"environment": {
"is_private_link": null,
"private_endpoint": null,
"subnet": null,
"utc_now": "2023-05-01T12:00:00Z"
},
"action": "Microsoft.Storage/storageAccounts/read",
"suboperation": null
}
""";
var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson);
Console.WriteLine($"RBAC condition allowed: {allowed}");
```

View File

@@ -0,0 +1,191 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
namespace Regorus.Tests;
[TestClass]
public class AliasRegistryTests
{
private const string AliasesJson = @"[{
""namespace"": ""Microsoft.Storage"",
""resourceTypes"": [{
""resourceType"": ""storageAccounts"",
""aliases"": [{
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
""paths"": []
}, {
""name"": ""Microsoft.Storage/storageAccounts/accessTier"",
""defaultPath"": ""properties.accessTier"",
""paths"": []
}]
}]
}]";
private const string ManifestJson = @"{
""dataNamespace"": ""Microsoft.KeyVault.Data"",
""aliases"": [],
""resourceTypeAliases"": [{
""resourceType"": ""vaults/certificates"",
""aliases"": [{
""name"": ""Microsoft.KeyVault.Data/vaults/certificates/keySize"",
""paths"": [{ ""path"": ""keySize"", ""apiVersions"": [""7.0""] }]
}]
}]
}";
[TestMethod]
public void Create_and_dispose_succeeds()
{
using var registry = new AliasRegistry();
Assert.AreEqual(0, registry.Length);
}
[TestMethod]
public void LoadJson_populates_registry()
{
using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
Assert.AreEqual(1, registry.Length);
}
[TestMethod]
public void LoadManifest_populates_registry()
{
using var registry = new AliasRegistry();
registry.LoadManifest(ManifestJson);
Assert.AreEqual(1, registry.Length);
}
[TestMethod]
public void NormalizeAndWrap_produces_envelope()
{
using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{
""name"": ""acct1"",
""type"": ""Microsoft.Storage/storageAccounts"",
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
}";
var result = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
Assert.IsNotNull(result);
var envelope = JsonNode.Parse(result!)!;
Assert.IsNotNull(envelope["resource"]);
Assert.IsNotNull(envelope["parameters"]);
Assert.IsNotNull(envelope["context"]);
// Normalized resource should have lowercased alias field names
var res = envelope["resource"]!;
Assert.AreEqual(true, res["supportshttpstrafficonly"]?.GetValue<bool>());
Assert.AreEqual("Hot", res["accesstier"]?.GetValue<string>());
Assert.AreEqual("acct1", res["name"]?.GetValue<string>());
}
[TestMethod]
public void NormalizeAndWrap_with_context_and_parameters()
{
using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{
""name"": ""acct1"",
""type"": ""Microsoft.Storage/storageAccounts"",
""properties"": { ""supportsHttpsTrafficOnly"": true }
}";
var context = @"{""resourceGroup"": {""name"": ""rg1""}}";
var parameters = @"{""env"": ""prod""}";
var result = registry.NormalizeAndWrap(resource, "2023-01-01", context, parameters);
Assert.IsNotNull(result);
var envelope = JsonNode.Parse(result!)!;
Assert.AreEqual("rg1", envelope["context"]!["resourceGroup"]!["name"]?.GetValue<string>());
Assert.AreEqual("prod", envelope["parameters"]!["env"]?.GetValue<string>());
}
[TestMethod]
public void Denormalize_restores_properties()
{
using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var normalized = @"{
""name"": ""acct1"",
""type"": ""Microsoft.Storage/storageAccounts"",
""supportshttpstrafficonly"": true,
""accesstier"": ""Hot""
}";
var result = registry.Denormalize(normalized, "2023-01-01");
Assert.IsNotNull(result);
var arm = JsonNode.Parse(result!)!;
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
}
[TestMethod]
public void Round_trip_normalize_then_denormalize()
{
using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{
""name"": ""acct1"",
""type"": ""Microsoft.Storage/storageAccounts"",
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
}";
// Normalize
var envelopeJson = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
Assert.IsNotNull(envelopeJson);
var envelope = JsonNode.Parse(envelopeJson!)!;
var normalizedResource = envelope["resource"]!.ToJsonString();
// Denormalize
var armJson = registry.Denormalize(normalizedResource, "2023-01-01");
Assert.IsNotNull(armJson);
var arm = JsonNode.Parse(armJson!)!;
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
}
[TestMethod]
public void DataPlane_manifest_normalize()
{
using var registry = new AliasRegistry();
registry.LoadManifest(ManifestJson);
var resource = @"{
""type"": ""Microsoft.KeyVault.Data/vaults/certificates"",
""keySize"": 2048
}";
var result = registry.NormalizeAndWrap(resource, "7.0", "{}", "{}");
Assert.IsNotNull(result);
var envelope = JsonNode.Parse(result!)!;
Assert.AreEqual(2048, envelope["resource"]!["keysize"]?.GetValue<int>());
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void LoadJson_invalid_throws()
{
using var registry = new AliasRegistry();
registry.LoadJson("not valid json");
}
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Linq;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
namespace Regorus.Tests;
[DoNotParallelize] // Uses global fallback config; must run sequentially.
[TestClass]
public class ExecutionTimerTests
{
private const string Policy = @"
package limits.timer
import rego.v1
triplet_count := count([1 |
x := data.values[_]
y := data.values[_]
z := data.values[_]
])
";
private const string Query = "data.limits.timer.triplet_count";
private const int ValueCount = 160;
[TestMethod]
public void Engine_limit_enforced()
{
Engine.ClearFallbackExecutionTimerConfig();
using var engine = CreateEngine(ValueCount);
var config = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
engine.SetExecutionTimerConfig(config);
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
StringAssert.Contains(ex.Message, "execution exceeded time limit");
}
[TestMethod]
public void Fallback_applies_to_new_engines()
{
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
Engine.SetFallbackExecutionTimerConfig(fallback);
try
{
using var engine = CreateEngine(ValueCount);
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
StringAssert.Contains(ex.Message, "execution exceeded time limit");
}
finally
{
Engine.ClearFallbackExecutionTimerConfig();
}
}
[TestMethod]
public void Engine_override_relaxes_fallback()
{
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
Engine.SetFallbackExecutionTimerConfig(fallback);
try
{
using var engine = CreateEngine(ValueCount);
var relaxed = new ExecutionTimerConfig(TimeSpan.FromSeconds(12), checkInterval: 1);
engine.SetExecutionTimerConfig(relaxed);
var resultJson = engine.EvalRule(Query);
var result = JsonSerializer.Deserialize<int>(resultJson!);
Assert.IsTrue(result > 0, "Expected a positive triplet count when limit is relaxed.");
engine.ClearExecutionTimerConfig();
var ex = Assert.ThrowsException<InvalidOperationException>(() => engine.EvalRule(Query));
StringAssert.Contains(ex.Message, "execution exceeded time limit");
}
finally
{
Engine.ClearFallbackExecutionTimerConfig();
}
}
[TestMethod]
public void CompiledPolicy_limit_enforced()
{
var fallback = new ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
Engine.SetFallbackExecutionTimerConfig(fallback);
try
{
using var policy = CreateCompiledPolicy(ValueCount);
var ex = Assert.ThrowsException<InvalidOperationException>(() => policy.EvalWithInput("null"));
StringAssert.Contains(ex.Message, "execution exceeded time limit");
}
finally
{
Engine.ClearFallbackExecutionTimerConfig();
}
}
[TestMethod]
public void CompiledPolicy_uses_engine_limits_only()
{
// Compiled policies no longer store per-policy execution timers; limits are managed by Engine.
Engine.ClearFallbackExecutionTimerConfig();
using var policy = CreateCompiledPolicy(ValueCount);
var resultJson = policy.EvalWithInput("null");
var result = JsonSerializer.Deserialize<int>(resultJson!);
Assert.IsTrue(result > 0, "CompiledPolicy should evaluate using engine defaults without its own timer");
}
private static Engine CreateEngine(int valueCount)
{
var engine = new Engine();
engine.AddPolicy("limits_timer.rego", Policy);
engine.AddDataJson(CreateData(valueCount));
return engine;
}
private static CompiledPolicy CreateCompiledPolicy(int valueCount)
{
var modules = new[] { new PolicyModule("limits_timer.rego", Policy) };
return Compiler.CompilePolicyWithEntrypoint(CreateData(valueCount), modules, Query);
}
private static string CreateData(int valueCount)
{
var payload = new { values = Enumerable.Range(0, valueCount).ToArray() };
return JsonSerializer.Serialize(payload);
}
}

View File

@@ -0,0 +1,320 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
namespace Regorus.Tests;
[TestClass]
[DoNotParallelize]
public class MemoryGrowthTests
{
private static int Iterations =>
int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_ITERS"), out var value) ? value : 50_000;
private static int LogEvery =>
int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_LOG_EVERY"), out var value) ? value : 500;
private static int GcEvery
{
get
{
if (!int.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_GC_EVERY"), out var value))
{
value = LogEvery;
}
return value <= 0 ? LogEvery : value;
}
}
private static long? MaxWorkingSetDeltaBytes
{
get
{
if (!long.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_MAX_DELTA_MB"), out var mb))
{
mb = 32;
}
if (mb <= 0)
{
return null;
}
return mb * 1024L * 1024L;
}
}
private static ulong? GlobalRegorusMemoryLimitBytes
{
get
{
if (!ulong.TryParse(Environment.GetEnvironmentVariable("REGORUS_MEMORY_TEST_GLOBAL_REGORUS_LIMIT_MB"), out var mb))
{
return null;
}
if (mb == 0)
{
return null;
}
return mb * 1024UL * 1024UL;
}
}
private static void WithOptionalGlobalRegorusMemoryLimit(Action action)
{
var priorLimit = MemoryLimits.GetGlobalMemoryLimit();
try
{
if (GlobalRegorusMemoryLimitBytes is { } limit)
{
MemoryLimits.SetGlobalMemoryLimit(limit);
}
action();
}
finally
{
MemoryLimits.SetGlobalMemoryLimit(priorLimit);
}
}
private static void ForceFullGc()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
[TestMethod]
public void Engine_create_eval_dispose_does_not_grow_working_set()
{
WithOptionalGlobalRegorusMemoryLimit(() =>
{
var process = Process.GetCurrentProcess();
process.Refresh();
var baseline = process.WorkingSet64;
var maxDelta = 0L;
var baselineManaged = GC.GetTotalMemory(false);
var maxManagedDelta = 0L;
for (var i = 1; i <= Iterations; i++)
{
using (var engine = new Engine())
{
engine.AddPolicy("test.rego", "package test\nx = 1\nmessage = `Hello`");
_ = engine.EvalRule("data.test.message");
}
if (i % LogEvery == 0)
{
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
var delta = workingSet - baseline;
var managedDelta = managed - baselineManaged;
if (delta > maxDelta)
{
maxDelta = delta;
}
if (managedDelta > maxManagedDelta)
{
maxManagedDelta = managedDelta;
}
Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n");
}
}
if (MaxWorkingSetDeltaBytes is { } limit)
{
Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n");
Assert.IsTrue(
maxDelta <= limit,
$"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB.");
}
});
}
[TestMethod]
public void Engine_create_eval_finalize_does_not_grow_working_set()
{
WithOptionalGlobalRegorusMemoryLimit(() =>
{
var process = Process.GetCurrentProcess();
process.Refresh();
var baseline = process.WorkingSet64;
var maxDelta = 0L;
var baselineManaged = GC.GetTotalMemory(false);
var maxManagedDelta = 0L;
for (var i = 1; i <= Iterations; i++)
{
var engine = new Engine();
engine.AddPolicy("test.rego", "package test\nx = 1\nmessage = `Hello`");
_ = engine.EvalRule("data.test.message");
if (i % GcEvery == 0)
{
ForceFullGc();
}
if (i % LogEvery == 0)
{
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
var delta = workingSet - baseline;
var managedDelta = managed - baselineManaged;
if (delta > maxDelta)
{
maxDelta = delta;
}
if (managedDelta > maxManagedDelta)
{
maxManagedDelta = managedDelta;
}
Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n");
}
}
if (MaxWorkingSetDeltaBytes is { } limit)
{
Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n");
Assert.IsTrue(
maxDelta <= limit,
$"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB.");
}
});
}
[TestMethod]
public void Rvm_rehydrate_execute_dispose_does_not_grow_working_set()
{
WithOptionalGlobalRegorusMemoryLimit(() =>
{
var modules = new[]
{
new PolicyModule("test.rego", "package test\nallow = true"),
};
using var compiled = Program.CompileFromModules("{}", modules, new[] { "data.test.allow" });
var serialized = compiled.SerializeBinary();
var process = Process.GetCurrentProcess();
process.Refresh();
var baseline = process.WorkingSet64;
var maxDelta = 0L;
var baselineManaged = GC.GetTotalMemory(false);
var maxManagedDelta = 0L;
for (var i = 1; i <= Iterations; i++)
{
using (var vm = new Rvm())
using (var program = Program.DeserializeBinary(serialized, out _))
{
vm.LoadProgram(program);
vm.SetDataJson("{}");
vm.SetInputJson("{}");
_ = vm.ExecuteEntryPoint(0);
}
if (i % LogEvery == 0)
{
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
var delta = workingSet - baseline;
var managedDelta = managed - baselineManaged;
if (delta > maxDelta)
{
maxDelta = delta;
}
if (managedDelta > maxManagedDelta)
{
maxManagedDelta = managedDelta;
}
Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n");
}
}
if (MaxWorkingSetDeltaBytes is { } limit)
{
Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n");
Assert.IsTrue(
maxDelta <= limit,
$"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB.");
}
});
}
[TestMethod]
public void Rvm_rehydrate_execute_finalize_does_not_grow_working_set()
{
WithOptionalGlobalRegorusMemoryLimit(() =>
{
var modules = new[]
{
new PolicyModule("test.rego", "package test\nallow = true"),
};
using var compiled = Program.CompileFromModules("{}", modules, new[] { "data.test.allow" });
var serialized = compiled.SerializeBinary();
var process = Process.GetCurrentProcess();
process.Refresh();
var baseline = process.WorkingSet64;
var maxDelta = 0L;
var baselineManaged = GC.GetTotalMemory(false);
var maxManagedDelta = 0L;
for (var i = 1; i <= Iterations; i++)
{
var vm = new Rvm();
var program = Program.DeserializeBinary(serialized, out _);
vm.LoadProgram(program);
vm.SetDataJson("{}");
vm.SetInputJson("{}");
_ = vm.ExecuteEntryPoint(0);
if (i % GcEvery == 0)
{
ForceFullGc();
}
if (i % LogEvery == 0)
{
process.Refresh();
var workingSet = process.WorkingSet64;
var managed = GC.GetTotalMemory(false);
var delta = workingSet - baseline;
var managedDelta = managed - baselineManaged;
if (delta > maxDelta)
{
maxDelta = delta;
}
if (managedDelta > maxManagedDelta)
{
maxManagedDelta = managedDelta;
}
Console.WriteLine($"\n\n\u001b[1m{i} ws_mb={workingSet / 1048576.0:F1} managed_mb={managed / 1048576.0:F1} delta_mb={delta / 1048576.0:F1}\u001b[0m\n\n");
}
}
if (MaxWorkingSetDeltaBytes is { } limit)
{
Console.WriteLine($"\n\n\u001b[1mSUMMARY: max ws delta {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB); max managed delta {maxManagedDelta / 1048576.0:F1} MB.\u001b[0m\n\n");
Assert.IsTrue(
maxDelta <= limit,
$"Working set grew by {maxDelta / 1048576.0:F1} MB (limit {limit / 1048576.0:F1} MB). Managed heap max delta {maxManagedDelta / 1048576.0:F1} MB.");
}
});
}
}

View File

@@ -0,0 +1,84 @@
#if REGORUS_FFI_TEST_HOOKS
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus.Internal;
namespace Regorus.Tests;
[TestClass]
public sealed class PanicGuardTests
{
[TestInitialize]
public void Initialize()
{
API.regorus_engine_test_reset_poison();
}
[TestCleanup]
public void Cleanup()
{
API.regorus_engine_test_reset_poison();
}
[TestMethod]
public void Panic_produces_invalid_operation_exception()
{
var panic = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
StringAssert.Contains(panic.Message, "panicked", "panic message should capture payload");
}
[TestMethod]
public void Poison_flag_blocks_subsequent_calls()
{
_ = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
var poisoned = Assert.ThrowsException<InvalidOperationException>(TriggerPanic);
StringAssert.Contains(poisoned.Message, "poisoned", "poisoned message should explain guard state");
}
private static unsafe void TriggerPanic()
{
var result = API.regorus_engine_test_trigger_panic();
try
{
if (result.status == RegorusStatus.Ok)
{
return;
}
var message = PtrToStringUtf8((IntPtr)result.error_message);
throw result.status.CreateException(message);
}
finally
{
API.regorus_result_drop(result);
}
}
private static string? PtrToStringUtf8(IntPtr ptr)
{
#if NETSTANDARD2_1
return Marshal.PtrToStringUTF8(ptr);
#else
if (ptr == IntPtr.Zero)
{
return null;
}
var len = 0;
while (Marshal.ReadByte(ptr, len) != 0)
{
len++;
}
var buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, buffer.Length);
return System.Text.Encoding.UTF8.GetString(buffer);
#endif
}
}
#endif

View File

@@ -0,0 +1,374 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
using YamlDotNet.Serialization;
namespace Regorus.Tests;
[TestClass]
public class RbacEngineTests
{
public TestContext? TestContext { get; set; }
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false
};
private const string BaseContextJson = """
{
"principal": {
"id": "user-1",
"principal_type": "User",
"custom_security_attributes": {
"department": "eng",
"levels": ["L1", "L2"]
}
},
"resource": {
"id": "/subscriptions/s1",
"resource_type": "Microsoft.Storage/storageAccounts",
"scope": "/subscriptions/s1",
"attributes": {
"owner": "alice",
"tags": ["a", "b"],
"count": 5,
"enabled": false,
"ip": "10.0.0.5",
"guid": "a1b2c3d4-0000-0000-0000-000000000000"
}
},
"request": {
"action": "Microsoft.Storage/storageAccounts/read",
"data_action": "Microsoft.Storage/storageAccounts/read",
"attributes": {
"owner": "alice",
"text": "HelloWorld",
"tags": ["prod", "gold"],
"count": 10,
"ratio": 2.5,
"enabled": true,
"ip": "10.0.0.8",
"guid": "A1B2C3D4-0000-0000-0000-000000000000",
"time": "12:30:15",
"date": "2023-05-01T12:00:00Z",
"numbers": [1, 2, 3],
"letters": ["a", "b"]
}
},
"environment": {
"is_private_link": false,
"private_endpoint": null,
"subnet": null,
"utc_now": "2023-05-01T12:00:00Z"
},
"action": "Microsoft.Storage/storageAccounts/read",
"suboperation": "sub/read"
}
""";
[TestMethod]
public void Rbac_engine_evaluates_all_yaml_cases()
{
var cases = LoadEvalTestCases().ToList();
Assert.IsTrue(cases.Count > 0, "No RBAC test cases were loaded.");
foreach (var testCase in cases)
{
TestContext?.WriteLine($"RBAC case: {testCase.Name} -> {testCase.Condition}");
var context = BuildBaseContext();
if (testCase.Context != null)
{
ApplyOverrides(context, testCase.Context);
}
var contextJson = context.ToJsonString(JsonOptions);
var result = RbacEngine.EvaluateCondition(testCase.Condition, contextJson);
Assert.AreEqual(
testCase.Expected,
result,
$"RBAC test '{testCase.Name}' failed for condition '{testCase.Condition}'.");
}
}
private static JsonObject BuildBaseContext()
{
var node = JsonNode.Parse(BaseContextJson) as JsonObject;
if (node is null)
{
throw new InvalidOperationException("Failed to parse base context JSON.");
}
return node;
}
private static void ApplyOverrides(JsonObject context, EvalContextOverrides overrides)
{
var principal = (JsonObject?)context["principal"]
?? throw new InvalidOperationException("Missing principal section.");
var resource = (JsonObject?)context["resource"]
?? throw new InvalidOperationException("Missing resource section.");
var request = (JsonObject?)context["request"]
?? throw new InvalidOperationException("Missing request section.");
var environment = (JsonObject?)context["environment"]
?? throw new InvalidOperationException("Missing environment section.");
if (!string.IsNullOrEmpty(overrides.Action))
{
context["action"] = overrides.Action;
}
if (!string.IsNullOrEmpty(overrides.Suboperation))
{
context["suboperation"] = overrides.Suboperation;
}
if (!string.IsNullOrEmpty(overrides.RequestAction))
{
request["action"] = overrides.RequestAction;
}
if (!string.IsNullOrEmpty(overrides.DataAction))
{
request["data_action"] = overrides.DataAction;
}
if (!string.IsNullOrEmpty(overrides.PrincipalId))
{
principal["id"] = overrides.PrincipalId;
}
if (!string.IsNullOrEmpty(overrides.PrincipalType))
{
principal["principal_type"] = overrides.PrincipalType;
}
if (!string.IsNullOrEmpty(overrides.ResourceId))
{
resource["id"] = overrides.ResourceId;
}
if (!string.IsNullOrEmpty(overrides.ResourceType))
{
resource["resource_type"] = overrides.ResourceType;
}
if (!string.IsNullOrEmpty(overrides.ResourceScope))
{
resource["scope"] = overrides.ResourceScope;
}
if (overrides.RequestAttributes != null)
{
request["attributes"] = ConvertToJsonNode(overrides.RequestAttributes);
}
if (overrides.ResourceAttributes != null)
{
resource["attributes"] = ConvertToJsonNode(overrides.ResourceAttributes);
}
if (overrides.PrincipalCustomSecurityAttributes != null)
{
principal["custom_security_attributes"] = ConvertToJsonNode(overrides.PrincipalCustomSecurityAttributes);
}
if (overrides.Environment != null)
{
if (overrides.Environment.IsPrivateLink.HasValue)
{
environment["is_private_link"] = overrides.Environment.IsPrivateLink.Value;
}
if (!string.IsNullOrEmpty(overrides.Environment.PrivateEndpoint))
{
environment["private_endpoint"] = overrides.Environment.PrivateEndpoint;
}
if (!string.IsNullOrEmpty(overrides.Environment.Subnet))
{
environment["subnet"] = overrides.Environment.Subnet;
}
if (!string.IsNullOrEmpty(overrides.Environment.UtcNow))
{
environment["utc_now"] = overrides.Environment.UtcNow;
}
}
}
private static IEnumerable<EvalTestCase> LoadEvalTestCases()
{
var baseDir = Path.Combine(AppContext.BaseDirectory, "test_cases");
if (!Directory.Exists(baseDir))
{
throw new DirectoryNotFoundException($"RBAC test case directory not found: {baseDir}");
}
var deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties()
.Build();
var files = Directory.EnumerateFiles(baseDir, "*.yaml")
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
foreach (var file in files)
{
var yaml = File.ReadAllText(file);
var suite = deserializer.Deserialize<EvalTestSuite>(yaml);
if (suite?.TestCases is null)
{
continue;
}
foreach (var testCase in suite.TestCases)
{
yield return testCase;
}
}
}
private static JsonNode? ConvertToJsonNode(object? value)
{
if (value is null)
{
return null;
}
switch (value)
{
case JsonNode node:
return node;
case string text:
return JsonValue.Create(text);
case bool boolean:
return JsonValue.Create(boolean);
case int intValue:
return JsonValue.Create(intValue);
case long longValue:
return JsonValue.Create(longValue);
case double doubleValue:
return JsonValue.Create(doubleValue);
case float floatValue:
return JsonValue.Create(floatValue);
case decimal decimalValue:
return JsonValue.Create(decimalValue);
case DateTime dateTime:
return JsonValue.Create(dateTime.ToString("O"));
case IDictionary dictionary:
{
var obj = new JsonObject();
foreach (DictionaryEntry entry in dictionary)
{
var key = entry.Key?.ToString() ?? string.Empty;
obj[key] = ConvertToJsonNode(entry.Value);
}
return obj;
}
case IEnumerable enumerable:
{
if (value is string)
{
return JsonValue.Create(value.ToString());
}
var array = new JsonArray();
foreach (var item in enumerable)
{
array.Add(ConvertToJsonNode(item));
}
return array;
}
default:
return JsonValue.Create(value.ToString());
}
}
private sealed class EvalTestSuite
{
[YamlMember(Alias = "test_cases")]
public List<EvalTestCase> TestCases { get; set; } = new();
}
private sealed class EvalTestCase
{
[YamlMember(Alias = "name")]
public string Name { get; set; } = string.Empty;
[YamlMember(Alias = "condition")]
public string Condition { get; set; } = string.Empty;
[YamlMember(Alias = "expected")]
public bool Expected { get; set; }
[YamlMember(Alias = "context")]
public EvalContextOverrides? Context { get; set; }
}
private sealed class EvalContextOverrides
{
[YamlMember(Alias = "action")]
public string? Action { get; set; }
[YamlMember(Alias = "suboperation")]
public string? Suboperation { get; set; }
[YamlMember(Alias = "request_action")]
public string? RequestAction { get; set; }
[YamlMember(Alias = "data_action")]
public string? DataAction { get; set; }
[YamlMember(Alias = "principal_id")]
public string? PrincipalId { get; set; }
[YamlMember(Alias = "principal_type")]
public string? PrincipalType { get; set; }
[YamlMember(Alias = "resource_id")]
public string? ResourceId { get; set; }
[YamlMember(Alias = "resource_type")]
public string? ResourceType { get; set; }
[YamlMember(Alias = "resource_scope")]
public string? ResourceScope { get; set; }
[YamlMember(Alias = "request_attributes")]
public object? RequestAttributes { get; set; }
[YamlMember(Alias = "resource_attributes")]
public object? ResourceAttributes { get; set; }
[YamlMember(Alias = "principal_custom_security_attributes")]
public object? PrincipalCustomSecurityAttributes { get; set; }
[YamlMember(Alias = "environment")]
public EvalEnvironmentOverrides? Environment { get; set; }
}
private sealed class EvalEnvironmentOverrides
{
[YamlMember(Alias = "is_private_link")]
public bool? IsPrivateLink { get; set; }
[YamlMember(Alias = "private_endpoint")]
public string? PrivateEndpoint { get; set; }
[YamlMember(Alias = "subnet")]
public string? Subnet { get; set; }
[YamlMember(Alias = "utc_now")]
public string? UtcNow { get; set; }
}
}

View File

@@ -6,6 +6,7 @@
<!-- More info about dotnet test integration https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-integration-dotnet-test -->
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
@@ -18,10 +19,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" Version="3.8.2" />
<PackageReference Include="MSTest" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)"/>
<PackageReference Include="Regorus" />
</ItemGroup>
<ItemGroup>
<None Include="../../../src/languages/azure_rbac/test_cases/*.yaml" Link="test_cases/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
@@ -11,6 +12,8 @@ namespace Regorus.Tests;
[TestClass]
public class RegorusTests
{
private static readonly object LimitLock = new();
[TestMethod]
public void Basic_evaluation_succeeds()
{
@@ -190,10 +193,19 @@ public class RegorusTests
var result = engine.GetPolicyPackageNames();
var packageNames = JsonNode.Parse(result!);
Assert.IsNotNull(result);
Assert.AreEqual("test", packageNames![0]["package_name"].ToString());
Assert.AreEqual("test.nested.name", packageNames![1]["package_name"].ToString());
var packageNames = JsonNode.Parse(result);
Assert.IsNotNull(packageNames);
var packageArray = packageNames.AsArray();
var firstPackage = packageArray[0]?.AsObject();
var secondPackage = packageArray[1]?.AsObject();
Assert.IsNotNull(firstPackage);
Assert.IsNotNull(secondPackage);
Assert.AreEqual("test", firstPackage!["package_name"]!.GetValue<string>());
Assert.AreEqual("test.nested.name", secondPackage!["package_name"]!.GetValue<string>());
}
[TestMethod]
@@ -206,51 +218,251 @@ public class RegorusTests
var result = engine.GetPolicyParameters();
var parameters = JsonNode.Parse(result!);
Assert.IsNotNull(result);
Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count);
Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count);
var parameters = JsonNode.Parse(result);
Assert.IsNotNull(parameters);
Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString());
Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString());
var parametersArray = parameters.AsArray();
var firstEntry = parametersArray[0]?.AsObject();
Assert.IsNotNull(firstEntry);
var parameterList = firstEntry!["parameters"]!.AsArray();
var modifierList = firstEntry["modifiers"]!.AsArray();
Assert.AreEqual(1, parameterList.Count);
Assert.AreEqual(1, modifierList.Count);
var parameterName = parameterList[0]?.AsObject()?["name"]?.GetValue<string>();
var modifierName = modifierList[0]?.AsObject()?["name"]?.GetValue<string>();
Assert.AreEqual("a", parameterName);
Assert.AreEqual("b", modifierName);
}
[TestMethod]
public void SetInputJson_has_negligible_allocations_after_warmup()
{
using var engine = new Engine();
const string payload = "{}";
// Warm up the engine and JIT to ensure subsequent measurements are representative.
for (int i = 0; i < 16; i++)
[TestMethod]
public void Global_memory_limit_can_be_set_and_cleared()
{
engine.SetInputJson(payload);
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
MemoryLimits.SetGlobalMemoryLimit(null);
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
const ulong limit = 32 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
Assert.AreEqual(limit, MemoryLimits.GetGlobalMemoryLimit());
MemoryLimits.SetGlobalMemoryLimit(null);
Assert.IsNull(MemoryLimits.GetGlobalMemoryLimit());
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
const int iterations = 256;
var before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
[TestMethod]
public void Memory_limit_violations_surface_from_engine_calls()
{
engine.SetInputJson(payload);
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var engine = new Engine();
const ulong limit = 1;
var payload = new string('x', 128 * 1024);
MemoryLimits.FlushThreadMemoryCounters();
MemoryLimits.SetGlobalMemoryLimit(limit);
try
{
var ex = Assert.ThrowsException<InvalidOperationException>(
() => engine.SetInputJson($"{{\"payload\":\"{payload}\"}}"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
}
finally
{
MemoryLimits.SetGlobalMemoryLimit(null);
MemoryLimits.FlushThreadMemoryCounters();
}
}
}
var after = GC.GetAllocatedBytesForCurrentThread();
var allocated = Math.Max(0, after - before);
var bytesPerOp = allocated / (double)iterations;
[TestMethod]
public void Evaluation_fails_when_input_pushes_policy_over_global_limit()
{
lock (LimitLock)
{
using var guard = new MemoryLimitScope();
using var engine = new Engine();
// Runtime bookkeeping (delegate caches, GC write barriers) differs across platforms, so
// we measure bytes per call rather than absolute totals and allow a small budget.
// CI will flag regressions where marshalling starts allocating per invocation.
const string policy = """
package memorylimit
// Allow a small budget for delegates and runtime bookkeeping while still flagging regressions.
Assert.IsTrue(
bytesPerOp <= 512,
$"Expected ≤512 B/op after warmup, but observed {bytesPerOp:F2} B/op (total {allocated} bytes)."
);
}
import rego.v1
stretched := concat("", [input.block | numbers.range(0, input.repeat - 1)[_]])
""";
engine.AddPolicy("memorylimit.rego", policy);
MemoryLimits.FlushThreadMemoryCounters();
const ulong limit = 4 * 1024 * 1024;
MemoryLimits.SetGlobalMemoryLimit(limit);
var block = new string('x', 16 * 1024);
var smallInput = JsonSerializer.Serialize(new { block, repeat = 16 });
engine.SetInputJson(smallInput);
var smallResult = engine.EvalRule("data.memorylimit.stretched");
Assert.IsNotNull(smallResult);
var stretched = JsonSerializer.Deserialize<string>(smallResult);
Assert.IsNotNull(stretched, "Policy should return a string result.");
Assert.AreEqual(block.Length * 16, stretched!.Length, "Policy should expand the payload under the limit.");
var largeInput = JsonSerializer.Serialize(new { block, repeat = 4096 });
engine.SetInputJson(largeInput);
var ex = Assert.ThrowsException<InvalidOperationException>(
() => engine.EvalRule("data.memorylimit.stretched"));
StringAssert.Contains(ex.Message, "execution exceeded memory limit");
}
}
[TestMethod]
public void Thread_flush_threshold_roundtrips()
{
lock (LimitLock)
{
var original = MemoryLimits.GetThreadMemoryFlushThreshold();
try
{
const ulong threshold = 256 * 1024;
MemoryLimits.SetThreadFlushThresholdOverride(threshold);
Assert.AreEqual(threshold, MemoryLimits.GetThreadMemoryFlushThreshold());
MemoryLimits.SetThreadFlushThresholdOverride(null);
var restored = MemoryLimits.GetThreadMemoryFlushThreshold();
Assert.IsTrue(restored.HasValue, "Clearing override should restore allocator default.");
if (original.HasValue)
{
Assert.AreEqual(original, restored);
}
}
finally
{
MemoryLimits.SetThreadFlushThresholdOverride(original);
}
}
}
[TestMethod]
public void SetInputJson_has_negligible_allocations_after_warmup()
{
using var engine = new Engine();
const string payload = "{}";
// Warm up the engine and JIT to ensure subsequent measurements are representative.
for (int i = 0; i < 16; i++)
{
engine.SetInputJson(payload);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
const int iterations = 256;
var before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
{
engine.SetInputJson(payload);
}
var after = GC.GetAllocatedBytesForCurrentThread();
var allocated = Math.Max(0, after - before);
var bytesPerOp = allocated / (double)iterations;
// Runtime bookkeeping (delegate caches, GC write barriers) differs across platforms, so
// we measure bytes per call rather than absolute totals and allow a small budget.
// CI will flag regressions where marshalling starts allocating per invocation.
// Allow a small budget for delegates and runtime bookkeeping while still flagging regressions.
Assert.IsTrue(
bytesPerOp <= 512,
$"Expected ≤512 B/op after warmup, but observed {bytesPerOp:F2} B/op (total {allocated} bytes)."
);
}
[TestMethod]
public void Disposed_objects_throw_object_disposed_exception()
{
var engine = new Engine();
engine.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => engine.EvalRule("data.test.message"));
var program = Program.CreateEmpty();
program.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => program.SerializeBinary());
var rvm = new Rvm();
rvm.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => rvm.Execute());
var modules = new[] { new PolicyModule("test.rego", "package test\nallow = true") };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.test.allow");
compiled.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => compiled.EvalWithInput("{}"));
}
[TestMethod]
public void Registry_helpers_return_empty_after_clear()
{
TargetRegistry.Clear();
Assert.IsTrue(TargetRegistry.IsEmpty);
Assert.AreEqual(0, TargetRegistry.GetNames().Count);
SchemaRegistry.ClearResources();
SchemaRegistry.ClearEffects();
Assert.IsTrue(SchemaRegistry.IsResourceRegistryEmpty);
Assert.IsTrue(SchemaRegistry.IsEffectRegistryEmpty);
Assert.AreEqual(0, SchemaRegistry.GetResourceNames().Count);
Assert.AreEqual(0, SchemaRegistry.GetEffectNames().Count);
}
[TestMethod]
public void Utf8_marshalling_handles_large_unicode_payloads()
{
var payload = string.Concat(new string('ß', 2048), "-✓-", new string('漢', 1024));
using var engine = new Engine();
engine.AddPolicy("test.rego", "package test\nmessage = input.msg");
engine.SetInputJson(JsonSerializer.Serialize(new { msg = payload }));
var result = engine.EvalRule("data.test.message");
Assert.IsNotNull(result);
// Compare by parsing the JSON string to avoid encoder differences across platforms.
var parsed = JsonSerializer.Deserialize<string>(result);
Assert.IsNotNull(parsed);
Assert.AreEqual(payload, parsed);
}
private sealed class MemoryLimitScope : IDisposable
{
private readonly ulong? _originalLimit;
public MemoryLimitScope()
{
_originalLimit = MemoryLimits.GetGlobalMemoryLimit();
}
public void Dispose()
{
MemoryLimits.SetGlobalMemoryLimit(_originalLimit);
MemoryLimits.FlushThreadMemoryCounters();
}
}
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1,153 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Manages Azure Policy alias definitions used for resource normalization
/// and policy compilation.
/// </summary>
public unsafe sealed class AliasRegistry : SafeHandleWrapper
{
/// <summary>
/// Create an empty alias registry.
/// </summary>
public AliasRegistry()
: base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry))
{
}
/// <summary>
/// Load control-plane alias data (array of ProviderAliases) from a JSON string.
/// </summary>
/// <param name="json">JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json)</param>
public void LoadJson(string json)
{
Utf8Marshaller.WithUtf8(json, jsonPtr =>
{
UseHandle(regPtr =>
{
CheckAndDropResult(API.regorus_alias_registry_load_json(
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
return 0;
});
});
}
/// <summary>
/// Load a data-plane policy manifest from a JSON string.
/// </summary>
/// <param name="json">JSON object containing a DataPolicyManifest</param>
public void LoadManifest(string json)
{
Utf8Marshaller.WithUtf8(json, jsonPtr =>
{
UseHandle(regPtr =>
{
CheckAndDropResult(API.regorus_alias_registry_load_manifest(
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
return 0;
});
});
}
/// <summary>
/// Gets the number of resource types loaded in the registry.
/// </summary>
public long Length
{
get
{
return UseHandle(regPtr =>
{
return ResultHelpers.GetIntResult(
API.regorus_alias_registry_len((RegorusAliasRegistry*)regPtr));
});
}
}
/// <summary>
/// Normalize an ARM resource JSON and wrap it into the standard input envelope
/// expected by a compiled Azure Policy program.
/// </summary>
/// <param name="resourceJson">Raw ARM resource JSON</param>
/// <param name="apiVersion">API version string (e.g. "2023-01-01"), or null to use default alias paths</param>
/// <param name="contextJson">Additional context JSON object (pass "{}" if none)</param>
/// <param name="parametersJson">Policy parameter values JSON (pass "{}" if none)</param>
/// <returns>JSON string: { "resource": &lt;normalized&gt;, "context": &lt;context&gt;, "parameters": &lt;params&gt; }</returns>
public string? NormalizeAndWrap(string resourceJson, string? apiVersion = null, string contextJson = "{}", string parametersJson = "{}")
{
return Utf8Marshaller.WithUtf8(resourceJson, resPtr =>
Utf8Marshaller.WithUtf8(contextJson, ctxPtr =>
Utf8Marshaller.WithUtf8(parametersJson, paramsPtr =>
{
if (apiVersion is null)
{
return UseHandle(regPtr =>
{
return ResultHelpers.GetStringResult(
API.regorus_alias_registry_normalize_and_wrap(
(RegorusAliasRegistry*)regPtr,
(byte*)resPtr, null,
(byte*)ctxPtr, (byte*)paramsPtr));
});
}
else
{
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
UseHandle(regPtr =>
{
return ResultHelpers.GetStringResult(
API.regorus_alias_registry_normalize_and_wrap(
(RegorusAliasRegistry*)regPtr,
(byte*)resPtr, (byte*)apiPtr,
(byte*)ctxPtr, (byte*)paramsPtr));
}));
}
})));
}
/// <summary>
/// Denormalize a previously-normalized resource JSON back to ARM format.
/// </summary>
/// <param name="normalizedJson">The normalized resource JSON</param>
/// <param name="apiVersion">API version string, or null to use default alias paths</param>
/// <returns>Denormalized ARM JSON string</returns>
public string? Denormalize(string normalizedJson, string? apiVersion = null)
{
return Utf8Marshaller.WithUtf8(normalizedJson, normPtr =>
{
if (apiVersion is null)
{
return UseHandle(regPtr =>
{
return ResultHelpers.GetStringResult(
API.regorus_alias_registry_denormalize(
(RegorusAliasRegistry*)regPtr,
(byte*)normPtr, null));
});
}
else
{
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
UseHandle(regPtr =>
{
return ResultHelpers.GetStringResult(
API.regorus_alias_registry_denormalize(
(RegorusAliasRegistry*)regPtr,
(byte*)normPtr, (byte*)apiPtr));
}));
}
});
}
private static string? CheckAndDropResult(RegorusResult result)
{
return ResultHelpers.GetStringResult(result);
}
}
}

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Regorus.Tests")]

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace Regorus
{
/// <summary>
/// Global configuration for compiled pattern caches used by regex and glob builtins.
/// </summary>
public readonly struct CacheConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="CacheConfig"/> struct.
/// </summary>
/// <param name="regex">Maximum cached compiled regex patterns (default 256, 0 = disabled).</param>
/// <param name="glob">Maximum cached compiled glob matchers (default 128, 0 = disabled).</param>
public CacheConfig(nuint regex, nuint glob)
{
Regex = regex;
Glob = glob;
}
/// <summary>Maximum cached compiled regex patterns (default 256).</summary>
public nuint Regex { get; }
/// <summary>Maximum cached compiled glob matchers (default 128).</summary>
public nuint Glob { get; }
internal Regorus.Internal.RegorusCacheConfig ToNative()
{
return new Regorus.Internal.RegorusCacheConfig
{
regex = Regex,
glob = Glob,
};
}
}
}

View File

@@ -2,9 +2,8 @@
// Licensed under the MIT License.
using System;
using System.Text;
using System.Text.Json;
using System.Threading;
using Regorus.Internal;
#nullable enable
namespace Regorus
@@ -18,20 +17,15 @@ namespace Regorus
/// Each instance represents a unique native policy object.
///
/// Thread Safety: This class is thread-safe for all operations. Multiple threads
/// can safely call EvalWithInput() concurrently, and Dispose() will safely wait
/// for all active evaluations to complete before freeing resources. No external
/// synchronization is required.
/// can safely call EvalWithInput() concurrently. Dispose() blocks new calls, waits
/// briefly, and defers the native release to the last in-flight caller if needed.
/// No external synchronization is required.
/// </summary>
public unsafe sealed class CompiledPolicy : IDisposable
public unsafe sealed class CompiledPolicy : SafeHandleWrapper
{
private RegorusCompiledPolicyHandle? _handle;
private readonly ManualResetEventSlim _idleEvent = new(initialState: true);
private int _isDisposed;
private int _activeEvaluations;
internal CompiledPolicy(RegorusCompiledPolicyHandle handle)
: base(handle, nameof(CompiledPolicy))
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
/// <summary>
@@ -45,36 +39,16 @@ namespace Regorus
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public string? EvalWithInput(string inputJson)
{
// Increment active evaluations count
var active = System.Threading.Interlocked.Increment(ref _activeEvaluations);
if (active == 1)
return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
{
_idleEvent.Reset();
}
try
{
ThrowIfDisposed();
return Internal.Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
return UseHandle(policyPtr =>
{
return UseHandle(policyPtr =>
unsafe
{
unsafe
{
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr));
}
});
return CheckAndDropResult(Internal.API.regorus_compiled_policy_eval_with_input((Internal.RegorusCompiledPolicy*)policyPtr, (byte*)inputPtr));
}
});
}
finally
{
// Decrement active evaluations count
var remaining = System.Threading.Interlocked.Decrement(ref _activeEvaluations);
if (remaining == 0)
{
_idleEvent.Set();
}
}
});
}
/// <summary>
@@ -86,7 +60,6 @@ namespace Regorus
/// <exception cref="ObjectDisposedException">Thrown when the policy has been disposed</exception>
public PolicyInfo GetPolicyInfo()
{
ThrowIfDisposed();
var jsonResult = UseHandle(policyPtr =>
{
unsafe
@@ -94,7 +67,7 @@ namespace Regorus
return CheckAndDropResult(Internal.API.regorus_compiled_policy_get_policy_info((Internal.RegorusCompiledPolicy*)policyPtr));
}
});
if (string.IsNullOrEmpty(jsonResult))
{
throw new Exception("Failed to get policy info: empty response");
@@ -106,8 +79,8 @@ namespace Regorus
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<PolicyInfo>(jsonResult!, options)
return JsonSerializer.Deserialize<PolicyInfo>(jsonResult!, options)
?? throw new Exception("Failed to deserialize policy info");
}
catch (JsonException ex)
@@ -116,105 +89,9 @@ namespace Regorus
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
var handle = _handle;
if (handle != null)
{
_idleEvent.Wait();
handle.Dispose();
_handle = null;
}
_idleEvent.Dispose();
}
}
private void ThrowIfDisposed()
{
if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
private string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private RegorusCompiledPolicyHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return handle;
}
private T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(CompiledPolicy));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
return Internal.ResultHelpers.GetStringResult(result);
}
}
}

View File

@@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Regorus.Internal;
#nullable enable
@@ -14,17 +12,17 @@ namespace Regorus
/// <summary>
/// Represents a policy module with an ID and content.
/// </summary>
public struct PolicyModule
public readonly struct PolicyModule
{
/// <summary>
/// Gets or sets the unique identifier for this policy module.
/// Gets the unique identifier for this policy module.
/// </summary>
public string Id { get; set; }
public string Id { get; }
/// <summary>
/// Gets or sets the Rego policy content.
/// Gets the Rego policy content.
/// </summary>
public string Content { get; set; }
public string Content { get; }
/// <summary>
/// Initializes a new instance of the PolicyModule struct.
@@ -55,50 +53,40 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IEnumerable<PolicyModule> modules, string entryPointRule)
{
var modulesArray = modules.ToArray();
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try
if (modules is null)
{
for (int i = 0; i < modulesArray.Length; i++)
throw new ArgumentNullException(nameof(modules));
}
return CompilePolicyWithEntrypoint(dataJson, modules.ToArray(), entryPointRule);
}
/// <summary>
/// Compiles a policy from data and modules with a specific entry point rule.
/// </summary>
public static CompiledPolicy CompilePolicyWithEntrypoint(string dataJson, IReadOnlyList<PolicyModule> modules, string entryPointRule)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr =>
{
var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id);
var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content);
pinnedStrings.Add(idPinned);
pinnedStrings.Add(contentPinned);
nativeModules[i] = new Internal.RegorusPolicyModule
unsafe
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
Utf8Marshaller.WithUtf8(entryPointRule, entryPointPtr =>
{
unsafe
fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
{
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_with_entrypoint(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length, (byte*)entryPointPtr);
var result = Internal.API.regorus_compile_policy_with_entrypoint(
(byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length, (byte*)entryPointPtr);
var policy = GetCompiledPolicyResult(result);
return policy;
}
return GetCompiledPolicyResult(result);
}
}));
}
finally
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
}
}
}));
}
/// <summary>
@@ -112,62 +100,39 @@ namespace Regorus
/// <exception cref="Exception">Thrown when compilation fails</exception>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IEnumerable<PolicyModule> modules)
{
var modulesArray = modules.ToArray();
var nativeModules = new Internal.RegorusPolicyModule[modulesArray.Length];
var pinnedStrings = new List<Utf8Marshaller.PinnedUtf8>(modulesArray.Length * 2);
try
if (modules is null)
{
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 Internal.RegorusPolicyModule
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
unsafe
{
fixed (Internal.RegorusPolicyModule* modulesPtr = nativeModules)
{
var result = Internal.API.regorus_compile_policy_for_target(
(byte*)dataPtr, modulesPtr, (UIntPtr)modulesArray.Length);
var policy = GetCompiledPolicyResult(result);
return policy;
}
}
});
}
finally
{
foreach (var pinned in pinnedStrings)
{
pinned.Dispose();
}
throw new ArgumentNullException(nameof(modules));
}
return CompilePolicyForTarget(dataJson, modules.ToArray());
}
private static string? StringFromUTF8(IntPtr ptr)
/// <summary>
/// Compiles a target-aware policy from data and modules.
/// </summary>
public static CompiledPolicy CompilePolicyForTarget(string dataJson, IReadOnlyList<PolicyModule> modules)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
using var pinnedModules = Internal.ModuleMarshalling.PinPolicyModules(modules);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
unsafe
{
fixed (Internal.RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
{
var result = Internal.API.regorus_compile_policy_for_target(
(byte*)dataPtr, modulesPtr, (UIntPtr)pinnedModules.Length);
return GetCompiledPolicyResult(result);
}
}
});
}
private static CompiledPolicy GetCompiledPolicyResult(Internal.RegorusResult result)
@@ -176,8 +141,8 @@ namespace Regorus
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown compilation error occurred");
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
if (result.data_type != Internal.RegorusDataType.Pointer || result.pointer_value == null)

View File

@@ -16,52 +16,42 @@ namespace Regorus
/// Cloning is cheap and involves only incrementing reference counts for shared immutable objects like parsed policies,
/// data etc. Mutable state is deep copied as needed.
/// </summary>
public unsafe sealed class Engine : IDisposable
public unsafe sealed class Engine : SafeHandleWrapper
{
private RegorusEngineHandle? _handle;
private int _isDisposed;
public Engine()
: base(RegorusEngineHandle.Create(), nameof(Engine))
{
_handle = RegorusEngineHandle.Create();
}
public void Dispose()
public static void SetFallbackExecutionTimerConfig(ExecutionTimerConfig config)
{
Dispose(disposing: true);
// This object will be cleaned up by the Dispose method.
// Therefore, call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
var nativeConfig = config.ToNative();
CheckAndDropResult(Regorus.Internal.API.regorus_set_fallback_execution_timer_config(nativeConfig));
}
// Dispose(bool disposing) executes in two distinct scenarios.
// If disposing equals true, the method has been called directly
// or indirectly by a user's code. Managed and unmanaged resources
// can be disposed.
// If disposing equals false, the method has been called by the
// runtime from inside the finalizer and you should not reference
// other objects. Only unmanaged resources can be disposed.
void Dispose(bool disposing)
public static void ClearFallbackExecutionTimerConfig()
{
if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
_handle?.Dispose();
_handle = null;
}
CheckAndDropResult(Regorus.Internal.API.regorus_clear_fallback_execution_timer_config());
}
public static void SetCacheConfig(CacheConfig config)
{
var nativeConfig = config.ToNative();
CheckAndDropResult(Regorus.Internal.API.regorus_set_cache_config(nativeConfig));
}
public static void ClearCache()
{
CheckAndDropResult(Regorus.Internal.API.regorus_clear_cache());
}
private Engine(RegorusEngineHandle handle)
: base(handle, nameof(Engine))
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
public Engine Clone()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
@@ -80,365 +70,216 @@ namespace Regorus
public void SetStrictBuiltinErrors(bool strict)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_strict_builtin_errors((Regorus.Internal.RegorusEngine*)enginePtr, strict));
});
}
public void SetExecutionTimerConfig(ExecutionTimerConfig config)
{
var nativeConfig = config.ToNative();
UseHandle(enginePtr =>
{
var localConfig = nativeConfig;
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr, &localConfig));
});
}
public void ClearExecutionTimerConfig()
{
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public void SetPolicyLengthConfig(PolicyLengthConfig config)
{
var nativeConfig = config.ToNative();
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr, nativeConfig));
});
}
public void ClearPolicyLengthConfig()
{
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? AddPolicy(string path, string rego)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(path, pathPtr =>
Utf8Marshaller.WithUtf8(rego, regoPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr));
}
});
}
}));
UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr, (byte*)regoPtr))
)));
}
public void SetRegoV0(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_rego_v0((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public string? AddPolicyFromFile(string path)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_from_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr))
);
});
}
public void AddDataJson(string data)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(data, dataPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)dataPtr));
});
});
}
public void AddDataFromJsonFile(string path)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_data_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
});
});
}
public void SetInputJson(string input)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(input, inputPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_json((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)inputPtr));
});
});
}
public void SetInputFromJsonFile(string path)
{
ThrowIfDisposed();
Utf8Marshaller.WithUtf8(path, pathPtr =>
{
unsafe
UseHandle(enginePtr =>
{
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
}
});
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_input_from_json_file((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)pathPtr));
});
});
}
public string? EvalQuery(string query)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(query, queryPtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_query((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)queryPtr))
);
});
}
public string? EvalRule(string rule)
{
ThrowIfDisposed();
return Utf8Marshaller.WithUtf8(rule, rulePtr =>
{
unsafe
{
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr));
}
});
}
return UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_eval_rule((Regorus.Internal.RegorusEngine*)enginePtr, (byte*)rulePtr))
);
});
}
public void SetEnableCoverage(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_enable_coverage((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public void ClearCoverageData()
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_coverage_data((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetCoverageReport()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetCoverageReportPretty()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_coverage_report_pretty((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public void SetGatherPrints(bool enable)
{
ThrowIfDisposed();
UseHandle(enginePtr =>
{
unsafe
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable));
}
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_gather_prints((Regorus.Internal.RegorusEngine*)enginePtr, enable));
});
}
public string? TakePrints()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_take_prints((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetAstAsJson()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetPolicyPackageNames()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? GetPolicyParameters()
{
ThrowIfDisposed();
return UseHandle(enginePtr =>
{
unsafe
{
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr));
}
return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
string? StringFromUTF8(IntPtr ptr)
private static string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
string? CheckAndDropResult(Regorus.Internal.RegorusResult result)
{
if (result.status != Regorus.Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
var ex = new Exception(message);
Regorus.Internal.API.regorus_result_drop(result);
throw ex;
}
var resultString = "";
if (result.output is not null)
{
resultString = StringFromUTF8((IntPtr)result.output);
}
Regorus.Internal.API.regorus_result_drop(result);
return resultString;
}
private void ThrowIfDisposed()
{
if (_isDisposed != 0 || _handle is null || _handle.IsClosed)
{
throw new ObjectDisposedException(nameof(Engine));
}
}
private RegorusEngineHandle GetHandleForUse()
{
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(nameof(Engine));
}
return handle;
}
private void UseHandle(Action<IntPtr> action)
{
UseHandle<object?>(handlePtr =>
{
action(handlePtr);
return null;
});
}
private T UseHandle<T>(Func<IntPtr, T> func)
{
var handle = GetHandleForUse();
bool addedRef = false;
try
{
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(Engine));
}
return func(pointer);
}
finally
{
if (addedRef)
{
handle.DangerousRelease();
}
}
return ResultHelpers.GetStringResult(result);
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace Regorus
{
/// <summary>
/// Managed representation of the execution timer configuration used by the engine.
/// </summary>
public readonly struct ExecutionTimerConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="ExecutionTimerConfig"/> struct.
/// </summary>
/// <param name="limit">Maximum wall-clock duration allowed for evaluation. Must be non-negative.</param>
/// <param name="checkInterval">Number of work units between timer checks. Must be non-zero.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="limit"/> is negative or <paramref name="checkInterval"/> is zero.</exception>
public ExecutionTimerConfig(TimeSpan limit, uint checkInterval)
{
if (limit < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(limit), "Execution timer limit must be non-negative.");
}
if (checkInterval == 0)
{
throw new ArgumentOutOfRangeException(nameof(checkInterval), "Execution timer check interval must be non-zero.");
}
Limit = limit;
CheckInterval = checkInterval;
}
/// <summary>
/// Maximum wall-clock duration allowed for an evaluation.
/// </summary>
public TimeSpan Limit { get; }
/// <summary>
/// Number of work units between timer checks.
/// </summary>
public uint CheckInterval { get; }
internal Regorus.Internal.RegorusExecutionTimerConfig ToNative()
{
if (Limit < TimeSpan.Zero)
{
throw new InvalidOperationException("Execution timer limit must be non-negative.");
}
ulong ticks = checked((ulong)Limit.Ticks);
ulong limitNanoseconds = checked(ticks * 100UL);
return new Regorus.Internal.RegorusExecutionTimerConfig
{
limit_ns = limitNanoseconds,
check_interval = CheckInterval,
};
}
}
}

View File

@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Helpers for configuring and inspecting Regorus memory limits via the native allocator bridge.
/// </summary>
public static class MemoryLimits
{
/// <summary>
/// Configure the process-wide global memory limit in bytes. Pass <c>null</c> to remove the limit.
/// </summary>
/// <param name="bytes">Maximum number of bytes the allocator may reserve before signalling an error.</param>
public static void SetGlobalMemoryLimit(ulong? bytes)
{
var result = API.regorus_set_global_memory_limit(bytes ?? 0, bytes.HasValue);
EnsureSuccess(result, nameof(SetGlobalMemoryLimit));
}
/// <summary>
/// Returns the currently configured global memory limit, if any.
/// </summary>
public static ulong? GetGlobalMemoryLimit()
{
var result = API.regorus_get_global_memory_limit();
return ExtractOptionalU64(result, "Failed to get global memory limit");
}
/// <summary>
/// Forces the allocator to flush this thread's pending counters into the global aggregates.
/// </summary>
public static void FlushThreadMemoryCounters()
{
var result = API.regorus_flush_thread_memory_counters();
EnsureSuccess(result, nameof(FlushThreadMemoryCounters));
}
/// <summary>
/// Immediately checks the global memory limit and throws if the allocator reports exhaustion.
/// </summary>
public static void CheckGlobalMemoryLimit()
{
var result = API.regorus_check_global_memory_limit();
EnsureSuccess(result, nameof(CheckGlobalMemoryLimit));
}
/// <summary>
/// Override the per-thread automatic flush threshold in bytes. Pass <c>null</c> to restore the default.
/// </summary>
public static void SetThreadFlushThresholdOverride(ulong? bytes)
{
var result = API.regorus_set_thread_flush_threshold_override(bytes ?? 0, bytes.HasValue);
EnsureSuccess(result, nameof(SetThreadFlushThresholdOverride));
}
/// <summary>
/// Returns the per-thread flush threshold, if automatic flushing is enabled.
/// </summary>
public static ulong? GetThreadMemoryFlushThreshold()
{
var result = API.regorus_get_thread_memory_flush_threshold();
return ExtractOptionalU64(result, "Failed to get thread memory flush threshold");
}
private static unsafe ulong? ExtractOptionalU64(RegorusResult result, string errorContext)
{
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message) ?? $"{errorContext}: native call failed";
throw result.status.CreateException(message);
}
if (!result.bool_value)
{
return null;
}
if (result.data_type != RegorusDataType.Integer)
{
throw new InvalidOperationException(
$"{errorContext}: native call returned {result.data_type} ({(int)result.data_type}) with bool_value={result.bool_value}"
);
}
try
{
return checked((ulong)result.int_value);
}
catch (OverflowException ex)
{
throw new OverflowException($"{errorContext}: native value was out of range ({result.int_value})", ex);
}
}
finally
{
API.regorus_result_drop(result);
}
}
private static void EnsureSuccess(RegorusResult result, string operation)
{
try
{
if (result.status != RegorusStatus.Ok)
{
string? message;
unsafe
{
message = Utf8Marshaller.FromUtf8(result.error_message);
}
throw result.status.CreateException(message);
}
}
finally
{
API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Generic;
using Regorus;
#nullable enable
namespace Regorus.Internal
{
internal static unsafe class ModuleMarshalling
{
internal sealed class PinnedPolicyModules : IDisposable
{
private readonly List<Utf8Marshaller.PinnedUtf8> _pins;
private bool _disposed;
internal PinnedPolicyModules(RegorusPolicyModule[] buffer, int length, List<Utf8Marshaller.PinnedUtf8> pins)
{
Buffer = buffer;
Length = length;
_pins = pins;
}
internal RegorusPolicyModule[] Buffer { get; }
internal int Length { get; }
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var pin in _pins)
{
pin.Dispose();
}
ArrayPool<RegorusPolicyModule>.Shared.Return(Buffer, clearArray: true);
_disposed = true;
}
}
internal sealed class PinnedEntryPoints : IDisposable
{
private readonly List<Utf8Marshaller.PinnedUtf8> _pins;
private bool _disposed;
internal PinnedEntryPoints(IntPtr[] buffer, int length, List<Utf8Marshaller.PinnedUtf8> pins)
{
Buffer = buffer;
Length = length;
_pins = pins;
}
internal IntPtr[] Buffer { get; }
internal int Length { get; }
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var pin in _pins)
{
pin.Dispose();
}
ArrayPool<IntPtr>.Shared.Return(Buffer, clearArray: true);
_disposed = true;
}
}
internal static PinnedPolicyModules PinPolicyModules(IReadOnlyList<PolicyModule> modules)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
var count = modules.Count;
var buffer = ArrayPool<RegorusPolicyModule>.Shared.Rent(count);
var pins = new List<Utf8Marshaller.PinnedUtf8>(count * 2);
try
{
for (int i = 0; i < count; i++)
{
var idPinned = Utf8Marshaller.Pin(modules[i].Id);
var contentPinned = Utf8Marshaller.Pin(modules[i].Content);
pins.Add(idPinned);
pins.Add(contentPinned);
buffer[i] = new RegorusPolicyModule
{
id = idPinned.Pointer,
content = contentPinned.Pointer
};
}
return new PinnedPolicyModules(buffer, count, pins);
}
catch
{
foreach (var pin in pins)
{
pin.Dispose();
}
ArrayPool<RegorusPolicyModule>.Shared.Return(buffer, clearArray: true);
throw;
}
}
internal static PinnedEntryPoints PinEntryPoints(IReadOnlyList<string> entryPoints)
{
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
var count = entryPoints.Count;
var buffer = ArrayPool<IntPtr>.Shared.Rent(count);
var pins = new List<Utf8Marshaller.PinnedUtf8>(count);
try
{
for (int i = 0; i < count; i++)
{
var entryPinned = Utf8Marshaller.Pin(entryPoints[i]);
pins.Add(entryPinned);
buffer[i] = (IntPtr)entryPinned.Pointer;
}
return new PinnedEntryPoints(buffer, count, pins);
}
catch
{
foreach (var pin in pins)
{
pin.Dispose();
}
ArrayPool<IntPtr>.Shared.Return(buffer, clearArray: true);
throw;
}
}
}
}

View File

@@ -26,6 +26,53 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_result_drop(RegorusResult result);
/// <summary>
/// Drop a RegorusBuffer.
/// data is not valid after drop.
/// </summary>
[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
/// <summary>
/// Set the global memory limit. Pass hasLimit=false to clear the limit.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_set_global_memory_limit", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_set_global_memory_limit(ulong limit, [MarshalAs(UnmanagedType.U1)] bool hasLimit);
/// <summary>
/// Get the current global memory limit. bool_value indicates whether a limit is set.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_get_global_memory_limit", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_get_global_memory_limit();
/// <summary>
/// Check the global memory limit immediately.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_check_global_memory_limit", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_check_global_memory_limit();
/// <summary>
/// Flush the current thread's pending allocation counters into global aggregates.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_flush_thread_memory_counters", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_flush_thread_memory_counters();
/// <summary>
/// Set the per-thread flush threshold override. Pass hasThreshold=false to restore defaults.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_set_thread_flush_threshold_override", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_set_thread_flush_threshold_override(ulong threshold, [MarshalAs(UnmanagedType.U1)] bool hasThreshold);
/// <summary>
/// Get the per-thread flush threshold. bool_value indicates whether a threshold is configured.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_get_thread_memory_flush_threshold", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_get_thread_memory_flush_threshold();
#endregion
#region Engine Methods
@@ -45,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);
/// <summary>
/// Compile an RVM program from the engine state with entry points.
/// </summary>
[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);
/// <summary>
/// Drop a RegorusEngine.
/// </summary>
@@ -52,6 +105,138 @@ namespace Regorus.Internal
internal static extern void regorus_engine_drop(RegorusEngine* engine);
/// <summary>
/// <summary>
/// Compile an RVM program from data/modules and entry points.
/// </summary>
[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);
/// <summary>
/// Construct a new empty program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusProgram* regorus_program_new();
/// <summary>
/// Drop a program handle.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_program_drop(RegorusProgram* program);
/// <summary>
/// Serialize a program to binary format.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_serialize_binary", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_serialize_binary(RegorusProgram* program);
/// <summary>
/// Deserialize a program from binary format.
/// </summary>
[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);
/// <summary>
/// Generate a readable assembly listing for the program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_program_generate_listing", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_program_generate_listing(RegorusProgram* program);
/// <summary>
/// Create a new RVM instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusRvm* regorus_rvm_new();
/// <summary>
/// Create a new RVM instance from a compiled policy.
/// </summary>
[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);
/// <summary>
/// Drop an RVM instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_rvm_drop(RegorusRvm* vm);
/// <summary>
/// Load a program into the RVM.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_load_program", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_load_program(RegorusRvm* vm, RegorusProgram* program);
/// <summary>
/// Set the data document for the RVM.
/// </summary>
[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);
/// <summary>
/// Set the input document for the RVM.
/// </summary>
[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);
/// <summary>
/// Execute the program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_execute(RegorusRvm* vm);
/// <summary>
/// Execute an entry point by name.
/// </summary>
[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);
/// <summary>
/// Execute an entry point by index.
/// </summary>
[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);
/// <summary>
/// Resume execution.
/// </summary>
[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);
/// <summary>
/// Get the current execution state.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_get_execution_state", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_get_execution_state(RegorusRvm* vm);
/// <summary>
/// Set the maximum instruction limit.
/// </summary>
[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);
/// <summary>
/// Set strict builtin error handling.
/// </summary>
[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);
/// <summary>
/// Set execution mode (0 run-to-completion, 1 suspendable).
/// </summary>
[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);
/// <summary>
/// Set step mode for suspendable execution.
/// </summary>
[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);
/// <summary>
/// Set execution timer configuration.
/// </summary>
[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
@@ -217,6 +402,76 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_compile_with_entrypoint", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_compile_with_entrypoint(RegorusEngine* engine, byte* rule);
#if REGORUS_FFI_TEST_HOOKS
/// <summary>
/// Trigger a panic inside the engine for testing purposes.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_trigger_panic", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_test_trigger_panic();
/// <summary>
/// Reset the engine poison flag for testing.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_test_reset_poison", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_engine_test_reset_poison();
#endif
/// <summary>
/// Configure the execution timer for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_execution_timer_config(RegorusEngine* engine, RegorusExecutionTimerConfig* config);
/// <summary>
/// Clear the execution timer configuration for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_execution_timer_config(RegorusEngine* engine);
/// <summary>
/// Set the policy length limits for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_policy_length_config(RegorusEngine* engine, RegorusPolicyLengthConfig config);
/// <summary>
/// Clear the policy length configuration for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_policy_length_config(RegorusEngine* engine);
#endregion
#region Execution Timer Global Methods
/// <summary>
/// Set the process-wide fallback execution timer configuration.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_set_fallback_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_set_fallback_execution_timer_config(RegorusExecutionTimerConfig config);
/// <summary>
/// Clear the process-wide fallback execution timer configuration.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_clear_fallback_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_clear_fallback_execution_timer_config();
#endregion
#region Cache Configuration Global Methods
/// <summary>
/// Configure the global pattern caches used by regex and glob builtins.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_set_cache_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_set_cache_config(RegorusCacheConfig config);
/// <summary>
/// Clear all entries from every pattern cache.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_clear_cache", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_clear_cache();
#endregion
#region Compilation Methods
@@ -265,6 +520,16 @@ namespace Regorus.Internal
#endregion
#region RBAC Methods
/// <summary>
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rbac_engine_eval_condition", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rbac_engine_eval_condition(byte* condition, byte* context_json);
#endregion
#region Target Registry Methods
/// <summary>
@@ -404,6 +669,55 @@ namespace Regorus.Internal
internal static extern RegorusResult regorus_effect_schema_clear();
#endregion
#region Alias Registry Methods
/// <summary>
/// Create a new, empty AliasRegistry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusAliasRegistry* regorus_alias_registry_new();
/// <summary>
/// Drop an AliasRegistry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry);
/// <summary>
/// Load control-plane alias data (array of ProviderAliases) into the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json);
/// <summary>
/// Load a data-plane policy manifest into the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json);
/// <summary>
/// Return the number of resource types loaded in the alias registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_len(RegorusAliasRegistry* registry);
/// <summary>
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
/// Returns a JSON string.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_normalize_and_wrap", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_normalize_and_wrap(
RegorusAliasRegistry* registry, byte* resource_json, byte* api_version, byte* context_json, byte* parameters_json);
/// <summary>
/// Denormalize a previously-normalized resource JSON back to ARM format.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_denormalize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_denormalize(
RegorusAliasRegistry* registry, byte* normalized_json, byte* api_version);
#endregion
}
#region Native Structures
@@ -472,6 +786,14 @@ namespace Regorus.Internal
/// Invalid policy content.
/// </summary>
InvalidPolicy,
/// <summary>
/// The engine panicked and cannot be reused until reset.
/// </summary>
Panic,
/// <summary>
/// The engine remains poisoned because a previous panic was detected.
/// </summary>
Poisoned,
}
/// <summary>
@@ -498,6 +820,7 @@ namespace Regorus.Internal
/// Boolean value.
/// Valid when data_type is Boolean.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool bool_value;
/// <summary>
/// Integer value.
@@ -516,6 +839,48 @@ namespace Regorus.Internal
public byte* error_message;
}
/// <summary>
/// FFI representation of the execution timer configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusExecutionTimerConfig
{
public ulong limit_ns;
public uint check_interval;
}
/// <summary>
/// FFI representation of the policy length configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusPolicyLengthConfig
{
public uint max_col;
public UIntPtr max_file_bytes;
public UIntPtr max_lines;
}
/// <summary>
/// FFI representation of the cache configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusCacheConfig
{
public UIntPtr regex;
public UIntPtr glob;
}
/// <summary>
/// Byte buffer returned from FFI.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct RegorusBuffer
{
public byte* data;
public UIntPtr len;
public UIntPtr capacity;
}
/// <summary>
/// Wrapper for regorus::Engine.
/// </summary>
@@ -532,6 +897,22 @@ namespace Regorus.Internal
{
}
/// <summary>
/// Wrapper for regorus::rvm::Program.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusProgram
{
}
/// <summary>
/// Wrapper for regorus::rvm::RegoVM.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusRvm
{
}
/// <summary>
/// FFI wrapper for PolicyModule struct.
/// </summary>
@@ -542,5 +923,13 @@ namespace Regorus.Internal
public byte* content;
}
/// <summary>
/// Wrapper for AliasRegistry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusAliasRegistry
{
}
#endregion
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace Regorus
{
/// <summary>
/// Policy source length limits enforced when loading policy files.
/// </summary>
public readonly struct PolicyLengthConfig
{
/// <summary>
/// Initializes a new instance of the <see cref="PolicyLengthConfig"/> struct.
/// </summary>
/// <param name="maxCol">Maximum column width per line. Must be non-zero.</param>
/// <param name="maxFileBytes">Maximum policy file size in bytes. Must be non-zero.</param>
/// <param name="maxLines">Maximum number of lines per policy file. Must be non-zero.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any parameter is zero.</exception>
public PolicyLengthConfig(uint maxCol, nuint maxFileBytes, nuint maxLines)
{
if (maxCol == 0)
throw new ArgumentOutOfRangeException(nameof(maxCol), "Must be non-zero.");
if (maxFileBytes == 0)
throw new ArgumentOutOfRangeException(nameof(maxFileBytes), "Must be non-zero.");
if (maxLines == 0)
throw new ArgumentOutOfRangeException(nameof(maxLines), "Must be non-zero.");
MaxCol = maxCol;
MaxFileBytes = maxFileBytes;
MaxLines = maxLines;
}
/// <summary>Maximum column width per line (default: 1024).</summary>
public uint MaxCol { get; }
/// <summary>Maximum policy file size in bytes (default: 1 MiB).</summary>
public nuint MaxFileBytes { get; }
/// <summary>Maximum number of lines per policy file (default: 20000).</summary>
public nuint MaxLines { get; }
internal Regorus.Internal.RegorusPolicyLengthConfig ToNative()
{
return new Regorus.Internal.RegorusPolicyLengthConfig
{
max_col = MaxCol,
max_file_bytes = MaxFileBytes,
max_lines = MaxLines,
};
}
}
}

View File

@@ -0,0 +1,249 @@
// 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
{
/// <summary>
/// Represents a compiled RVM program.
/// </summary>
public unsafe sealed class Program : SafeHandleWrapper
{
private Program(RegorusProgramHandle handle)
: base(handle, nameof(Program))
{
}
/// <summary>
/// Create an empty program.
/// </summary>
public static Program CreateEmpty()
{
return new Program(RegorusProgramHandle.Create());
}
/// <summary>
/// Compile an RVM program from modules and entry points.
/// </summary>
public static Program CompileFromModules(string dataJson, IEnumerable<PolicyModule> modules, IEnumerable<string> entryPoints)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
return CompileFromModules(dataJson, modules.ToArray(), entryPoints.ToArray());
}
/// <summary>
/// Compile an RVM program from modules and entry points.
/// </summary>
public static Program CompileFromModules(string dataJson, IReadOnlyList<PolicyModule> modules, IReadOnlyList<string> entryPoints)
{
if (modules is null)
{
throw new ArgumentNullException(nameof(modules));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
if (entryPoints.Count == 0)
{
throw new ArgumentException("At least one entry point is required.", nameof(entryPoints));
}
using var pinnedModules = ModuleMarshalling.PinPolicyModules(modules);
using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints);
return Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
fixed (RegorusPolicyModule* modulesPtr = pinnedModules.Buffer)
fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer)
{
var result = API.regorus_program_compile_from_modules(
(byte*)dataPtr,
modulesPtr,
(UIntPtr)pinnedModules.Length,
(byte**)entryPtr,
(UIntPtr)pinnedEntryPoints.Length);
return GetProgramResult(result);
}
});
}
/// <summary>
/// Compile an RVM program from an engine instance and entry points.
/// </summary>
public static Program CompileFromEngine(Engine engine, IEnumerable<string> entryPoints)
{
if (engine is null)
{
throw new ArgumentNullException(nameof(engine));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
return CompileFromEngine(engine, entryPoints.ToArray());
}
/// <summary>
/// Compile an RVM program from an engine instance and entry points.
/// </summary>
public static Program CompileFromEngine(Engine engine, IReadOnlyList<string> entryPoints)
{
if (engine is null)
{
throw new ArgumentNullException(nameof(engine));
}
if (entryPoints is null)
{
throw new ArgumentNullException(nameof(entryPoints));
}
if (entryPoints.Count == 0)
{
throw new ArgumentException("At least one entry point is required.", nameof(entryPoints));
}
using var pinnedEntryPoints = ModuleMarshalling.PinEntryPoints(entryPoints);
return engine.UseHandleForInterop(enginePtr =>
{
fixed (IntPtr* entryPtr = pinnedEntryPoints.Buffer)
{
var result = API.regorus_engine_compile_program_with_entrypoints(
(RegorusEngine*)enginePtr,
(byte**)entryPtr,
(UIntPtr)pinnedEntryPoints.Length);
return GetProgramResult(result);
}
});
}
/// <summary>
/// Deserialize an RVM program from binary format.
/// </summary>
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;
}
}
/// <summary>
/// Serialize the program to binary format.
/// </summary>
public byte[] SerializeBinary()
{
return UseHandle(programPtr =>
{
var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr);
return ExtractBuffer(result);
});
}
/// <summary>
/// Generate a readable assembly listing.
/// </summary>
public string? GenerateListing()
{
return UseHandle(programPtr =>
{
return CheckAndDropResult(API.regorus_program_generate_listing((RegorusProgram*)programPtr));
});
}
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)
{
return ResultHelpers.GetStringResult(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);
}
}
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Provides helpers for evaluating Azure RBAC condition expressions.
/// </summary>
public static unsafe class RbacEngine
{
/// <summary>
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
/// </summary>
/// <param name="condition">Azure RBAC condition expression.</param>
/// <param name="contextJson">JSON encoded EvaluationContext.</param>
/// <returns>True if the condition evaluates to true; otherwise false.</returns>
/// <exception cref="Exception">Thrown when evaluation fails.</exception>
public static bool EvaluateCondition(string condition, string contextJson)
{
if (condition is null)
{
throw new ArgumentNullException(nameof(condition));
}
if (contextJson is null)
{
throw new ArgumentNullException(nameof(contextJson));
}
return Utf8Marshaller.WithUtf8(condition, conditionPtr =>
Utf8Marshaller.WithUtf8(contextJson, contextPtr =>
{
unsafe
{
var result = Internal.API.regorus_rbac_engine_eval_condition((byte*)conditionPtr, (byte*)contextPtr);
return ResultHelpers.GetBoolResult(result);
}
}));
}
}
}

View File

@@ -8,22 +8,39 @@
<LangVersion>10.0</LangVersion>
<!-- See https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack -->
<VersionPrefix>0.8.0</VersionPrefix>
<VersionPrefix>0.9.1</VersionPrefix>
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT AND Apache-2.0 AND BSD-3-Clause</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/microsoft/regorus</PackageProjectUrl>
<RepositoryUrl>https://github.com/microsoft/regorus</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Authors>Microsoft</Authors>
<Company>Microsoft</Company>
<PackageTags>rego;policy;engine;authorization;opa;rust</PackageTags>
<Description>Fast, lightweight Rego interpreter and policy engine for .NET, powered by Rust.</Description>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
</PropertyGroup>
<PropertyGroup>
<RegorusFFIArtifactsProfile Condition="'$(RegorusFFIArtifactsProfile)' == ''">release</RegorusFFIArtifactsProfile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" />
</ItemGroup>
<PropertyGroup Condition="'$(EnableRegorusTestHooks)' == 'true'">
<DefineConstants>$(DefineConstants);REGORUS_FFI_TEST_HOOKS</DefineConstants>
</PropertyGroup>
<!--
$(RegorusFFIArtifactsDir) is the location where regorus shared libraries have been
built for various platforms and copied to. RegorusFFIArtifactsDir is passed in
by the publishing pipeline.
For each target triple, `Pack` expects the regorus ffi shared library
to be found in $(RegorusFFIArtifactsDir)/<target-triple>/release.
to be found in $(RegorusFFIArtifactsDir)/<target-triple>/$(RegorusFFIArtifactsProfile).
If $(IgnoreMissingArtifacts) is not set, ensure that the binaries for officially supported platforms exists.
-->
@@ -31,27 +48,31 @@
<Error Text="RegorusFFIArtifactsDir must be supplied." Condition="$(RegorusFFIArtifactsDir) == ''" />
<!-- Ensure that the binaries for officially supported platforms exists. -->
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/regorus_ffi.dll missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/regorus_ffi.dll')" />
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/regorus_ffi.pdb missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/regorus_ffi.pdb')" />
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/regorus_ffi.dll missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/regorus_ffi.dll')" />
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/regorus_ffi.pdb missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/regorus_ffi.pdb')" />
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/release/libregorus_ffi.so missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/release/libregorus_ffi.so')" />
<Error Text="$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/$(RegorusFFIArtifactsProfile)/libregorus_ffi.so missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/$(RegorusFFIArtifactsProfile)/libregorus_ffi.so')" />
<Error Text="$(RegorusFFIArtifactsDir)/aarch64-apple-darwin/$(RegorusFFIArtifactsProfile)/libregorus_ffi.dylib missing."
Condition="!Exists('$(RegorusFFIArtifactsDir)/aarch64-apple-darwin/$(RegorusFFIArtifactsProfile)/libregorus_ffi.dylib')" />
</Target>
<ItemGroup>
<None Include="docs/README.md" Pack="true" PackagePath="/" />
<None Include="../../../LICENSE" Pack="true" PackagePath="/" />
<!-- Copy each binary to expected location within the package -->
<None Include="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/*.dll" Pack="true" PackagePath="runtimes/win-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/release/*.pdb" Pack="true" PackagePath="runtimes/win-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/*.dll" Pack="true" PackagePath="runtimes/win-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/x86_64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/*.pdb" Pack="true" PackagePath="runtimes/win-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-pc-windows-msvc/release/*.dll" Pack="true" PackagePath="runtimes/win-arm64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-pc-windows-msvc/release/*.pdb" Pack="true" PackagePath="runtimes/win-arm64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/*.dll" Pack="true" PackagePath="runtimes/win-arm64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-pc-windows-msvc/$(RegorusFFIArtifactsProfile)/*.pdb" Pack="true" PackagePath="runtimes/win-arm64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/release/lib*.so" Pack="true" PackagePath="runtimes/linux-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/x86_64-unknown-linux-gnu/$(RegorusFFIArtifactsProfile)/lib*.so" Pack="true" PackagePath="runtimes/linux-x64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-apple-darwin/release/lib*.dylib" Pack="true" PackagePath="runtimes/osx-arm64/native/" />
<None Include="$(RegorusFFIArtifactsDir)/aarch64-apple-darwin/$(RegorusFFIArtifactsProfile)/lib*.dylib" Pack="true" PackagePath="runtimes/osx-arm64/native/" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
#nullable enable
namespace Regorus.Internal
{
internal static unsafe class ResultHelpers
{
internal static string? GetStringResult(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);
}
}
internal static bool GetBoolResult(RegorusResult result)
{
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type == RegorusDataType.Boolean && result.bool_value;
}
finally
{
API.regorus_result_drop(result);
}
}
internal static long GetIntResult(RegorusResult result)
{
try
{
if (result.status != RegorusStatus.Ok)
{
var message = Utf8Marshaller.FromUtf8(result.error_message);
throw result.status.CreateException(message);
}
return result.data_type == RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
API.regorus_result_drop(result);
}
}
}
}

View File

@@ -0,0 +1,227 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Execution mode for the RVM runtime.
/// </summary>
public enum ExecutionMode : byte
{
/// <summary>
/// Run to completion without yielding.
/// </summary>
RunToCompletion = 0,
/// <summary>
/// Suspendable execution mode.
/// </summary>
Suspendable = 1,
}
/// <summary>
/// Wrapper for the Regorus RVM runtime.
/// </summary>
public unsafe sealed class Rvm : SafeHandleWrapper
{
public Rvm()
: base(RegorusRvmHandle.Create(), nameof(Rvm))
{
}
private Rvm(RegorusRvmHandle handle)
: base(handle, nameof(Rvm))
{
}
/// <summary>
/// Create an RVM instance backed by a compiled policy (for default rule evaluation).
/// </summary>
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);
});
}
/// <summary>
/// Load a program into the VM.
/// </summary>
public void LoadProgram(Program program)
{
if (program is null)
{
throw new ArgumentNullException(nameof(program));
}
program.UseHandleForInterop(programPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_load_program((RegorusRvm*)vmPtr, (RegorusProgram*)programPtr));
return 0;
});
return 0;
});
}
/// <summary>
/// Set the data document for the VM.
/// </summary>
public void SetDataJson(string dataJson)
{
Utf8Marshaller.WithUtf8(dataJson, dataPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_data((RegorusRvm*)vmPtr, (byte*)dataPtr));
return 0;
});
});
}
/// <summary>
/// Set the input document for the VM.
/// </summary>
public void SetInputJson(string inputJson)
{
Utf8Marshaller.WithUtf8(inputJson, inputPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_input((RegorusRvm*)vmPtr, (byte*)inputPtr));
return 0;
});
});
}
/// <summary>
/// Set the execution mode (0 = run-to-completion, 1 = suspendable).
/// </summary>
public void SetExecutionMode(byte mode)
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode));
return 0;
});
}
/// <summary>
/// Set the execution mode.
/// </summary>
public void SetExecutionMode(ExecutionMode mode)
{
SetExecutionMode((byte)mode);
}
/// <summary>
/// Execute the program and return the JSON result.
/// </summary>
public string? Execute()
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute((RegorusRvm*)vmPtr));
});
}
/// <summary>
/// Execute a named entry point.
/// </summary>
public string? ExecuteEntryPoint(string entryPoint)
{
return Utf8Marshaller.WithUtf8(entryPoint, entryPtr =>
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_name((RegorusRvm*)vmPtr, (byte*)entryPtr));
});
});
}
/// <summary>
/// Execute an entry point by index.
/// </summary>
public string? ExecuteEntryPoint(ulong index)
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index((RegorusRvm*)vmPtr, (UIntPtr)index));
});
}
/// <summary>
/// Resume execution with an optional value.
/// </summary>
public string? Resume(string? resumeValueJson)
{
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));
});
});
}
/// <summary>
/// Get the current execution state.
/// </summary>
public string? GetExecutionState()
{
return UseHandle(vmPtr =>
{
return CheckAndDropResult(API.regorus_rvm_get_execution_state((RegorusRvm*)vmPtr));
});
}
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)
{
return ResultHelpers.GetStringResult(result);
}
}
}

View File

@@ -0,0 +1,272 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Runtime.InteropServices;
#nullable enable
namespace Regorus
{
/// <summary>
/// Base class for native handle wrappers that coordinates handle usage and disposal.
///
/// Behavior summary:
/// - UseHandle: blocks Dispose while running; throws ObjectDisposedException if disposal has started or the handle is invalid.
/// - Dispose: marks disposing and blocks new calls; waits briefly for in-flight calls to finish, then defers native release to the last exiting call if needed.
/// - Handles are never exposed directly; derived classes can only work through UseHandle helpers.
///
/// Concurrency model:
/// - _state tracks lifecycle transitions (Active -> DisposeRequested -> Released).
/// - HandleGate tracks in-flight operations and enforces the "no new calls after Dispose" rule.
/// - SafeHandle is pinned per call via DangerousAddRef to prevent use-after-free while native work runs.
/// - If Dispose times out, the last in-flight caller performs the release to avoid leaks.
/// </summary>
public abstract class SafeHandleWrapper : IDisposable
{
private static readonly TimeSpan DefaultDisposeTimeout = TimeSpan.FromMilliseconds(50);
private const int StateActive = 0;
private const int StateDisposeRequested = 1;
private const int StateReleased = 2;
private readonly HandleGate _gate;
private readonly string _ownerName;
private int _state;
private SafeHandle? _handle;
protected SafeHandleWrapper(SafeHandle handle, string ownerName)
{
// Cache ownership info and initialize the gate before any use to avoid racing disposal.
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
_ownerName = ownerName ?? throw new ArgumentNullException(nameof(ownerName));
_gate = new HandleGate(ownerName);
// Default to a very short wait when in-flight calls exist; release is deferred to the last caller if needed.
}
protected void UseHandle(Action<IntPtr> action)
{
// Reuse the generic path to keep add/ref/release in one place.
UseHandle<object?>(ptr =>
{
action(ptr);
return null;
});
}
protected T UseHandle<T>(Func<IntPtr, T> func)
{
// Fast reject if dispose was requested.
if (System.Threading.Volatile.Read(ref _state) != StateActive)
{
throw new ObjectDisposedException(_ownerName);
}
// Enter gate so Dispose waits for in-flight native calls.
_gate.Enter();
bool addedRef = false;
SafeHandle? handle = null;
try
{
// Race: Dispose could begin after Enter; GetHandleForUse validates the handle again.
handle = GetHandleForUse();
// DangerousAddRef pins the SafeHandle so Dispose cannot close it mid-call.
handle.DangerousAddRef(ref addedRef);
var pointer = handle.DangerousGetHandle();
// Validate pointer after AddRef in case handle became invalid between checks.
if (pointer == IntPtr.Zero)
{
throw new ObjectDisposedException(_ownerName);
}
return func(pointer);
}
finally
{
// Always release the DangerousAddRef to avoid leaking the native handle.
if (addedRef)
{
handle?.DangerousRelease();
}
// Leave gate so Dispose can proceed when the last caller exits.
var idle = _gate.Exit();
// Race: Dispose may have timed out while we were in-flight.
// The last exiting caller performs the native release to avoid leaks.
if (idle && System.Threading.Volatile.Read(ref _state) == StateDisposeRequested)
{
TryReleaseHandle();
}
}
}
internal T UseHandleForInterop<T>(Func<IntPtr, T> func)
{
// Explicit alias for interop-specific call sites.
return UseHandle(func);
}
internal void UseHandleForInterop(Action<IntPtr> action)
{
// Explicit alias for interop-specific call sites.
UseHandle(action);
}
private void ThrowIfDisposed()
{
// Fast check for dispose state so callers fail deterministically.
if (System.Threading.Volatile.Read(ref _state) != StateActive)
{
throw new ObjectDisposedException(_ownerName);
}
// Validate the underlying SafeHandle is still usable; avoids races with release.
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(_ownerName);
}
}
private SafeHandle GetHandleForUse()
{
// Centralized gate for derived classes to grab the handle safely.
// This is a second line of defense in case disposal began after the initial state check.
var handle = _handle;
if (handle is null || handle.IsClosed || handle.IsInvalid)
{
throw new ObjectDisposedException(_ownerName);
}
return handle;
}
public void Dispose()
{
// Only the first caller runs disposal; others become no-ops.
if (System.Threading.Interlocked.CompareExchange(ref _state, StateDisposeRequested, StateActive) == StateActive)
{
// Block new calls and wait briefly if there are in-flight operations.
var completed = _gate.TryBeginDispose(DefaultDisposeTimeout, out var hadActive);
if (completed)
{
// Either no active calls or they drained within the short timeout.
TryReleaseHandle();
}
else
{
// Defer release to the last in-flight caller to avoid leaks without blocking indefinitely.
// Race: if the last in-flight caller already exited, there will be no Exit() to trigger release.
// Re-check active state and release immediately in that case.
if (!hadActive || _gate.IsIdle)
{
TryReleaseHandle();
}
}
}
GC.SuppressFinalize(this);
}
private void TryReleaseHandle()
{
if (System.Threading.Interlocked.CompareExchange(ref _state, StateReleased, StateDisposeRequested) != StateDisposeRequested)
{
return;
}
// Once released, no caller should be able to observe a valid handle.
// SafeHandle.Dispose closes the native resource; null to prevent reuse after dispose.
_handle?.Dispose();
_handle = null;
// Release the wait handle resources after disposal completes.
_gate.Dispose();
}
/// <summary>
/// Tracks in-flight operations and coordinates disposal.
/// </summary>
private sealed class HandleGate : IDisposable
{
private readonly string _ownerName;
private readonly System.Threading.ManualResetEventSlim _idle = new(initialState: true);
private int _active;
private int _disposing;
internal HandleGate(string ownerName)
{
_ownerName = ownerName;
}
internal void Enter()
{
// If disposal already started, reject new work immediately.
if (System.Threading.Volatile.Read(ref _disposing) != 0)
{
ThrowDisposed();
}
// Track active callers; first one resets idle event.
var active = System.Threading.Interlocked.Increment(ref _active);
if (active == 1)
{
_idle.Reset();
}
// Re-check disposing to handle races where Dispose began after increment.
if (System.Threading.Volatile.Read(ref _disposing) != 0)
{
Exit();
ThrowDisposed();
}
}
internal bool Exit()
{
// Last caller signals idle so Dispose can continue.
if (System.Threading.Interlocked.Decrement(ref _active) == 0)
{
_idle.Set();
return true;
}
return false;
}
internal bool IsIdle => System.Threading.Volatile.Read(ref _active) == 0;
internal bool TryBeginDispose(TimeSpan timeout, out bool hadActive)
{
// Set disposing flag once; subsequent calls treat as already disposing.
if (System.Threading.Interlocked.Exchange(ref _disposing, 1) != 0)
{
hadActive = System.Threading.Volatile.Read(ref _active) != 0;
return true;
}
hadActive = System.Threading.Volatile.Read(ref _active) != 0;
if (!hadActive)
{
// No in-flight callers; disposal can proceed without waiting.
return true;
}
// Wait for active callers to drain; optional timeout avoids blocking forever.
if (timeout == System.Threading.Timeout.InfiniteTimeSpan)
{
_idle.Wait();
return true;
}
// Race note: callers may finish between the timeout decision and Wait call; Wait handles that safely.
return _idle.Wait(timeout);
}
private void ThrowDisposed()
{
throw new ObjectDisposedException(_ownerName);
}
public void Dispose()
{
_idle.Dispose();
}
}
}
}

View File

@@ -44,7 +44,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{
@@ -76,7 +76,7 @@ namespace Regorus
protected override bool ReleaseHandle()
{
if (!IsInvalid && !IsClosed)
if (!IsInvalid)
{
unsafe
{
@@ -87,4 +87,148 @@ 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)
{
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)
{
unsafe
{
Internal.API.regorus_rvm_drop((Internal.RegorusRvm*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusAliasRegistryHandle() : base(ownsHandle: true)
{
}
internal static RegorusAliasRegistryHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_alias_registry_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus alias registry.");
}
var handle = new RegorusAliasRegistryHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusAliasRegistryHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
unsafe
{
Internal.API.regorus_alias_registry_drop((Internal.RegorusAliasRegistry*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
}

View File

@@ -2,7 +2,8 @@
// Licensed under the MIT License.
using System;
using System.Text;
using System.Collections.Generic;
using System.Text.Json;
using Regorus.Internal;
#nullable enable
@@ -28,7 +29,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
@@ -47,7 +48,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_resource_schema_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -62,7 +63,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_resource_schema_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
@@ -76,7 +77,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_resource_schema_is_empty();
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
}
@@ -87,7 +88,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListResourceNames()
{
return CheckAndDropResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_list_names()) ?? "[]";
}
/// <summary>
/// List all registered resource schema names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetResourceNames()
{
var json = ListResourceNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -103,7 +113,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_resource_schema_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -114,7 +124,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void ClearResources()
{
CheckAndDropResult(Internal.API.regorus_resource_schema_clear());
ResultHelpers.GetStringResult(Internal.API.regorus_resource_schema_clear());
}
/// <summary>
@@ -131,7 +141,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_register((byte*)namePtr, (byte*)schemaPtr));
}
});
});
@@ -150,7 +160,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_effect_schema_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -165,7 +175,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_effect_schema_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
@@ -179,7 +189,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_effect_schema_is_empty();
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
}
@@ -190,7 +200,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListEffectNames()
{
return CheckAndDropResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_list_names()) ?? "[]";
}
/// <summary>
/// List all registered effect schema names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetEffectNames()
{
var json = ListEffectNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -206,7 +225,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_effect_schema_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -217,81 +236,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void ClearEffects()
{
CheckAndDropResult(Internal.API.regorus_effect_schema_clear());
}
private static string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static bool GetBoolResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static long GetIntResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
Internal.API.regorus_result_drop(result);
}
ResultHelpers.GetStringResult(Internal.API.regorus_effect_schema_clear());
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
#nullable enable
namespace Regorus.Internal
{
internal static class StatusExtensions
{
internal static Exception CreateException(this RegorusStatus status, string? message)
{
var details = string.IsNullOrWhiteSpace(message) ? "Regorus call failed." : message;
return status switch
{
RegorusStatus.Panic => new InvalidOperationException($"Regorus engine panicked: {details}"),
RegorusStatus.Poisoned => new InvalidOperationException($"Regorus engine is poisoned: {details}"),
_ => new InvalidOperationException(details),
};
}
}
}

View File

@@ -2,7 +2,8 @@
// Licensed under the MIT License.
using System;
using System.Text;
using System.Collections.Generic;
using System.Text.Json;
using Regorus.Internal;
#nullable enable
@@ -27,7 +28,7 @@ namespace Regorus
{
unsafe
{
CheckAndDropResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr));
ResultHelpers.GetStringResult(Internal.API.regorus_register_target_from_json((byte*)targetPtr));
}
});
}
@@ -45,7 +46,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_target_registry_contains((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -57,7 +58,16 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static string ListNames()
{
return CheckAndDropResult(Internal.API.regorus_target_registry_list_names()) ?? "[]";
return ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_list_names()) ?? "[]";
}
/// <summary>
/// Get a list of all registered target names as managed strings.
/// </summary>
public static IReadOnlyList<string> GetNames()
{
var json = ListNames();
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
}
/// <summary>
@@ -73,7 +83,7 @@ namespace Regorus
unsafe
{
var result = Internal.API.regorus_target_registry_remove((byte*)namePtr);
return GetBoolResult(result);
return ResultHelpers.GetBoolResult(result);
}
});
}
@@ -84,7 +94,7 @@ namespace Regorus
/// <exception cref="Exception">Thrown when the operation fails</exception>
public static void Clear()
{
CheckAndDropResult(Internal.API.regorus_target_registry_clear());
ResultHelpers.GetStringResult(Internal.API.regorus_target_registry_clear());
}
/// <summary>
@@ -97,10 +107,9 @@ namespace Regorus
get
{
var result = Internal.API.regorus_target_registry_len();
return GetIntResult(result);
return ResultHelpers.GetIntResult(result);
}
}
/// <summary>
/// Check if the target registry is empty.
/// </summary>
@@ -111,81 +120,7 @@ namespace Regorus
get
{
var result = Internal.API.regorus_target_registry_is_empty();
return GetBoolResult(result);
}
}
private static string? StringFromUTF8(IntPtr ptr)
{
#if NETSTANDARD2_1
return System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ptr);
#else
int len = 0;
while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, len) != 0) { ++len; }
byte[] buffer = new byte[len];
System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
#endif
}
private static string? CheckAndDropResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type switch
{
Internal.RegorusDataType.String => StringFromUTF8((IntPtr)result.output),
Internal.RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(),
Internal.RegorusDataType.Integer => result.int_value.ToString(),
Internal.RegorusDataType.None => null,
_ => StringFromUTF8((IntPtr)result.output)
};
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static bool GetBoolResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Boolean ? result.bool_value : false;
}
finally
{
Internal.API.regorus_result_drop(result);
}
}
private static long GetIntResult(Internal.RegorusResult result)
{
try
{
if (result.status != Internal.RegorusStatus.Ok)
{
var message = StringFromUTF8((IntPtr)result.error_message);
throw new Exception(message ?? "Unknown error occurred");
}
return result.data_type == Internal.RegorusDataType.Integer ? result.int_value : 0;
}
finally
{
Internal.API.regorus_result_drop(result);
return ResultHelpers.GetBoolResult(result);
}
}
}

View File

@@ -17,10 +17,10 @@ namespace Regorus.Internal
/// </summary>
internal static class Utf8Marshaller
{
// Mirrors BCL patterns (e.g., System.Text.Json encoding helpers) by stackalloc'ing
// up to 512 bytes to cover common short strings while keeping the stack usage well
// below typical per-frame limits; larger payloads fall back to pooled buffers.
private const int StackAllocThreshold = 512;
// Mirrors BCL patterns (e.g., System.Text.Json encoding helpers) by stackalloc'ing
// up to 512 bytes to cover common short strings while keeping the stack usage well
// below typical per-frame limits; larger payloads fall back to pooled buffers.
private const int StackAllocThreshold = 512;
/// <summary>
/// Represents a pooled and pinned UTF-8 buffer suitable for scenarios where
@@ -150,5 +150,48 @@ namespace Regorus.Internal
{
return new PinnedUtf8(value);
}
internal static unsafe string? FromUtf8(byte* pointer)
{
if (pointer is null)
{
return null;
}
#if NETSTANDARD2_1
return Marshal.PtrToStringUTF8((IntPtr)pointer);
#else
var intPtr = (IntPtr)pointer;
var length = 0;
while (Marshal.ReadByte(intPtr, length) != 0)
{
length++;
}
if (length == 0)
{
return string.Empty;
}
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
Marshal.Copy(intPtr, buffer, 0, length);
return Encoding.UTF8.GetString(buffer, 0, length);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
#endif
}
internal static string? FromUtf8(IntPtr pointer)
{
unsafe
{
return FromUtf8((byte*)pointer);
}
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Linq;
using System.Text.Json;
namespace TargetExampleApp;
@@ -50,6 +51,69 @@ import rego.v1
parameters.requiredTLSVersion = ""TLS1_2""
parameters.allowedPorts = [""22"", ""3389""]";
private const string EXECUTION_TIMER_POLICY = @"
package limits.timer
import rego.v1
triplet_count := count([1 |
x := data.values[_]
y := data.values[_]
z := data.values[_]
])
";
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"",
@@ -152,10 +216,22 @@ parameters.allowedPorts = [""22"", ""3389""]";
var nonCompliantResult = compiledPolicy.EvalWithInput(NON_COMPLIANT_STORAGE_ACCOUNT);
Console.WriteLine($"Result: {nonCompliantResult}");
// 4. Demonstrate thread-safe concurrent evaluation
Console.WriteLine("\n4. Testing concurrent evaluation from multiple threads:");
DemonstrateConcurrentEvaluation(compiledPolicy);
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)
@@ -170,42 +246,44 @@ parameters.allowedPorts = [""22"", ""3389""]";
};
Console.WriteLine($"Starting {testInputs.Length} concurrent evaluations...");
var tasks = testInputs.Select(input =>
Task.Run(() => {
var tasks = testInputs.Select(input =>
Task.Run(() =>
{
var (threadName, json) = input;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Multiple evaluations per thread to stress test
var results = new List<string>();
for (int i = 0; i < 1000; i++)
{
var result = compiledPolicy.EvalWithInput(json);
var result = compiledPolicy.EvalWithInput(json)
?? throw new System.InvalidOperationException("Expected EvalWithInput to return a JSON value.");
results.Add(result);
}
stopwatch.Stop();
var microseconds = stopwatch.ElapsedTicks * 1000000 / System.Diagnostics.Stopwatch.Frequency;
// Verify all results are identical (thread safety)
var firstResult = results[0];
var allIdentical = results.All(r => r == firstResult);
Console.WriteLine($"✓ {threadName}: {results.Count} evaluations in {microseconds}μs, " +
$"Results consistent: {allIdentical}");
return (threadName, results.Count, microseconds, allIdentical);
})
).ToArray();
// Wait for all threads to complete
var results = Task.WhenAll(tasks).Result;
Console.WriteLine("\nConcurrency test results:");
var totalEvaluations = results.Sum(r => r.Item2);
var maxTime = results.Max(r => r.Item3);
var allConsistent = results.All(r => r.allIdentical);
Console.WriteLine($"✓ Total evaluations: {totalEvaluations}");
Console.WriteLine($"✓ Max thread time: {maxTime}μs");
Console.WriteLine($"✓ All threads consistent: {allConsistent}");
@@ -216,28 +294,28 @@ parameters.allowedPorts = [""22"", ""3389""]";
static void DemonstratePolicyInfo(Regorus.CompiledPolicy compiledPolicy)
{
Console.WriteLine("Getting policy metadata using GetPolicyInfo()...");
try
{
var policyInfo = compiledPolicy.GetPolicyInfo();
Console.WriteLine($"✓ Policy Information Retrieved:");
Console.WriteLine($" Target Name: {policyInfo.TargetName ?? "None"}");
Console.WriteLine($" Effect Rule: {policyInfo.EffectRule ?? "None"}");
Console.WriteLine($" Entrypoint Rule: {policyInfo.EntrypointRule}");
Console.WriteLine($" Module IDs ({policyInfo.ModuleIds.Count}):");
foreach (var moduleId in policyInfo.ModuleIds)
{
Console.WriteLine($" - {moduleId}");
}
Console.WriteLine($" Applicable Resource Types ({policyInfo.ApplicableResourceTypes.Count}):");
foreach (var resourceType in policyInfo.ApplicableResourceTypes)
{
Console.WriteLine($" - {resourceType}");
}
if (policyInfo.Parameters != null && policyInfo.Parameters.Count > 0)
{
Console.WriteLine($" Policy Parameters:");
@@ -257,7 +335,7 @@ parameters.allowedPorts = [""22"", ""3389""]";
Console.WriteLine($" Description: {param.Description}");
}
}
if (parameterSet.Modifiers.Count > 0)
{
Console.WriteLine($" Modifiers ({parameterSet.Modifiers.Count}):");
@@ -272,11 +350,11 @@ parameters.allowedPorts = [""22"", ""3389""]";
{
Console.WriteLine(" No parameter information available");
}
// Demonstrate JSON serialization of policy info
Console.WriteLine("\n✓ Policy Info as JSON:");
var jsonOptions = new JsonSerializerOptions
{
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
@@ -288,4 +366,130 @@ parameters.allowedPorts = [""22"", ""3389""]";
Console.WriteLine($"✗ Failed to get policy info: {ex.Message}");
}
}
static void DemonstrateExecutionTimer()
{
var dataJson = JsonSerializer.Serialize(new
{
values = Enumerable.Range(0, EXECUTION_TIMER_VALUE_COUNT).ToArray()
});
var fallback = new Regorus.ExecutionTimerConfig(TimeSpan.FromMilliseconds(2), checkInterval: 1);
var relaxed = new Regorus.ExecutionTimerConfig(TimeSpan.FromMilliseconds(1000), checkInterval: 1);
Console.WriteLine($" Configuring fallback timer (limit={fallback.Limit.TotalMilliseconds:F0} ms, interval={fallback.CheckInterval})...");
Regorus.Engine.SetFallbackExecutionTimerConfig(fallback);
try
{
using var engine = new Regorus.Engine();
engine.AddPolicy("limits_timer.rego", EXECUTION_TIMER_POLICY);
engine.AddDataJson(dataJson);
Console.WriteLine(" Evaluating under fallback limit (expected failure)...");
try
{
engine.EvalRule(EXECUTION_TIMER_QUERY);
Console.WriteLine(" ⚠ Evaluation unexpectedly succeeded under fallback limit.");
}
catch (Exception ex)
{
Console.WriteLine($" ✓ Fallback enforced: {ex.Message}");
}
Console.WriteLine($" Applying per-engine override ({relaxed.Limit.TotalMilliseconds:F0} ms) and retrying...");
engine.SetExecutionTimerConfig(relaxed);
var result = engine.EvalRule(EXECUTION_TIMER_QUERY);
Console.WriteLine($" ✓ Override succeeded; triplet_count = {result}");
Console.WriteLine(" Clearing engine override to restore fallback...");
engine.ClearExecutionTimerConfig();
try
{
engine.EvalRule(EXECUTION_TIMER_QUERY);
Console.WriteLine(" ⚠ Evaluation unexpectedly succeeded after clearing override.");
}
catch (Exception ex)
{
Console.WriteLine($" ✓ Fallback restored: {ex.Message}");
}
}
finally
{
Regorus.Engine.ClearFallbackExecutionTimerConfig();
}
}
static void DemonstrateRvmUsage()
{
var modules = new List<Regorus.PolicyModule>
{
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<Regorus.PolicyModule>
{
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}");
}
}

View File

@@ -11,10 +11,15 @@
<PropertyGroup>
<!-- If the environment variable is set (such as in a Github Action run), append the suffix to the version number -->
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
<UseLocalRegorus Condition="'$(UseLocalRegorus)' == ''">false</UseLocalRegorus>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Regorus" Version="0.8.0$(RegorusPackageVersionSuffix)"/>
<ItemGroup Condition="'$(UseLocalRegorus)' == 'true'">
<ProjectReference Include="../Regorus/Regorus.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(UseLocalRegorus)' != 'true'">
<PackageReference Include="Regorus" />
</ItemGroup>
<ItemGroup>

View File

@@ -18,8 +18,13 @@ var w = new Stopwatch();
w.Restart();
// Configure the global pattern caches.
Regorus.Engine.SetCacheConfig(new Regorus.CacheConfig(regex: 256, glob: 128));
var engine = new Regorus.Engine();
engine.SetRegoV0(true);
// Raise the default col limit to 2000
engine.SetPolicyLengthConfig(new Regorus.PolicyLengthConfig(maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000));
w.Stop();
var newEngineTicks = w.ElapsedTicks;
@@ -42,7 +47,8 @@ w.Restart();
// Set input and eval rule.
engine.SetInputFromJsonFile("../../../tests/aci/input.json");
var value = engine.EvalRule("data.framework.mount_overlay");
var value = engine.EvalRule("data.framework.mount_overlay")
?? throw new System.InvalidOperationException("Expected EvalRule to return a JSON value.");
#if NET8_0_OR_GREATER
var valueDoc = System.Text.Json.JsonDocument.Parse(value);

View File

@@ -10,7 +10,17 @@
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="regorus" Version="0.8.0"/>
<PropertyGroup>
<!-- Allow CI to append the version suffix for locally built packages -->
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
<UseLocalRegorus Condition="'$(UseLocalRegorus)' == ''">false</UseLocalRegorus>
</PropertyGroup>
<ItemGroup Condition="'$(UseLocalRegorus)' == 'true'">
<ProjectReference Include="../Regorus/Regorus.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(UseLocalRegorus)' != 'true'">
<PackageReference Include="Regorus" />
</ItemGroup>
</Project>

View File

@@ -1,10 +1,10 @@
{
"msbuild-sdks": {
"Microsoft.Build.NoTargets": "3.7.56"
},
"sdk": {
"allowPrerelease": false,
"version": "8.0.412",
"rollForward": "latestFeature"
}
"msbuild-sdks": {
"Microsoft.Build.NoTargets": "3.7.134"
},
"sdk": {
"allowPrerelease": false,
"version": "8.0.412",
"rollForward": "latestFeature"
}
}

701
bindings/ffi/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@
[package]
name = "regorus-ffi"
version = "0.5.0"
version = "0.9.1"
edition = "2021"
license = "MIT AND Apache-2.0 AND BSD-3-Clause"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
@@ -24,13 +25,29 @@ lto = true
codegen-units = 1
[features]
default = ["ast", "azure_policy", "std", "coverage", "regorus/arc", "regorus/full-opa", "contention_checks"]
default = [
"ast",
"azure_policy",
"std",
"coverage",
"allocator-memory-limits",
"rvm",
"rbac",
"regorus/arc",
"regorus/full-opa",
"cache",
"contention_checks",
]
ast = ["regorus/ast"]
azure_policy = ["regorus/azure_policy"]
std = ["regorus/std"]
coverage = ["regorus/coverage"]
allocator-memory-limits = ["regorus/allocator-memory-limits"]
contention_checks = ["parking_lot"]
rvm = ["regorus/rvm"]
rbac = ["regorus/azure-rbac"]
cache = ["regorus/cache"]
custom_allocator = []
[build-dependencies]
cbindgen = "0.28.0"
cbindgen = "0.29.2"

View File

@@ -0,0 +1,479 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! FFI bindings for `AliasRegistry` Azure Policy alias catalog management.
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use anyhow::Result;
use core::ffi::c_char;
use core::ptr;
use regorus::languages::azure_policy::aliases::AliasRegistry;
/// Opaque wrapper for `AliasRegistry`.
pub struct RegorusAliasRegistry {
registry: AliasRegistry,
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
/// Create a new, empty `AliasRegistry`.
///
/// The caller must eventually call `regorus_alias_registry_drop` to free the handle.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry {
let wrapper = RegorusAliasRegistry {
registry: AliasRegistry::new(),
};
Box::into_raw(Box::new(wrapper))
}
/// Drop a `RegorusAliasRegistry`.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) {
if let Ok(r) = to_ref(registry) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(r));
}
}
}
// ---------------------------------------------------------------------------
// Loading
// ---------------------------------------------------------------------------
/// Load control-plane alias data (array of `ProviderAliases`) into the registry.
///
/// `json` must be a valid null-terminated UTF-8 string containing the JSON
/// array returned by `Get-AzPolicyAlias` or the static
/// `ResourceTypesAndAliases.json` file.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_load_json(
registry: *mut RegorusAliasRegistry,
json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<()> {
let json_str = from_c_str(json)?;
to_ref(registry)?.registry.load_from_json(&json_str)?;
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to load alias catalog: {e}"),
),
}
})
}
/// Load a data-plane policy manifest into the registry.
///
/// `json` must be a valid null-terminated UTF-8 string containing a single
/// `DataPolicyManifest` JSON object.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_load_manifest(
registry: *mut RegorusAliasRegistry,
json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<()> {
let json_str = from_c_str(json)?;
to_ref(registry)?
.registry
.load_data_policy_manifest_json(&json_str)?;
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to load data-plane manifest: {e}"),
),
}
})
}
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
/// Return the number of resource types loaded in the alias registry.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<i64> {
let len = to_ref(registry)?.registry.len();
Ok(len as i64)
}();
match output {
Ok(n) => RegorusResult::ok_int(n),
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
}
})
}
// ---------------------------------------------------------------------------
// Normalize / Denormalize
// ---------------------------------------------------------------------------
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
///
/// Returns a JSON string:
/// `{ "resource": <normalized>, "context": <context>, "parameters": <params> }`.
///
/// * `resource_json` raw ARM resource JSON
/// * `api_version` API version string (e.g. `"2023-01-01"`), or null to use
/// the default alias paths
/// * `context_json` JSON object for additional context (pass `"{}"` if none)
/// * `parameters_json` JSON object of policy parameter values (pass `"{}"` if none)
#[no_mangle]
pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
registry: *mut RegorusAliasRegistry,
resource_json: *const c_char,
api_version: *const c_char,
context_json: *const c_char,
parameters_json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let resource_str = from_c_str(resource_json)?;
let api_ver = if api_version.is_null() {
None
} else {
let s = from_c_str(api_version)?;
if s.is_empty() {
None
} else {
Some(s)
}
};
let context_str = from_c_str(context_json)?;
let params_str = from_c_str(parameters_json)?;
let resource = regorus::Value::from_json_str(&resource_str)?;
let context = regorus::Value::from_json_str(&context_str)?;
let params = regorus::Value::from_json_str(&params_str)?;
let wrapped = to_ref(registry)?.registry.normalize_and_wrap(
&resource,
api_ver.as_deref(),
Some(context),
Some(params),
);
wrapped.to_json_str()
}();
match output {
Ok(s) => RegorusResult::ok_string(s),
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
}
})
}
/// Denormalize a previously-normalized resource JSON back to ARM format.
///
/// * `normalized_json` the normalized resource JSON
/// * `api_version` API version string, or null to use the default alias paths
///
/// Returns the denormalized ARM JSON string.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_denormalize(
registry: *mut RegorusAliasRegistry,
normalized_json: *const c_char,
api_version: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<String> {
let normalized_str = from_c_str(normalized_json)?;
let api_ver = if api_version.is_null() {
None
} else {
let s = from_c_str(api_version)?;
if s.is_empty() {
None
} else {
Some(s)
}
};
let normalized = regorus::Value::from_json_str(&normalized_str)?;
let result = to_ref(registry)?
.registry
.denormalize(&normalized, api_ver.as_deref());
result.to_json_str()
}();
match output {
Ok(s) => RegorusResult::ok_string(s),
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::regorus_result_drop;
use core::ffi::CStr;
use std::ffi::CString;
/// Helper: create a C string from a Rust &str.
fn c(s: &str) -> CString {
CString::new(s).expect("CString::new failed")
}
/// Helper: assert a RegorusResult has Ok status and extract string output.
fn assert_ok_string(r: &RegorusResult) -> String {
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
assert!(!r.output.is_null(), "expected non-null output");
let s = unsafe { CStr::from_ptr(r.output) }
.to_str()
.expect("invalid UTF-8 in output")
.to_string();
s
}
/// Helper: assert a RegorusResult has Ok status with integer output.
fn assert_ok_int(r: &RegorusResult) -> i64 {
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
r.int_value
}
const ALIASES: &str = r#"[{
"namespace": "Microsoft.Storage",
"resourceTypes": [{
"resourceType": "storageAccounts",
"aliases": [{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
}]
}]
}]"#;
const MANIFEST: &str = r#"{
"dataNamespace": "Microsoft.KeyVault.Data",
"aliases": [],
"resourceTypeAliases": [{
"resourceType": "vaults/certificates",
"aliases": [{
"name": "Microsoft.KeyVault.Data/vaults/certificates/keySize",
"paths": [{ "path": "keySize", "apiVersions": ["7.0"] }]
}]
}]
}"#;
#[test]
fn lifecycle_new_and_drop() {
let reg = regorus_alias_registry_new();
assert!(!reg.is_null());
regorus_alias_registry_drop(reg);
}
#[test]
fn load_json_and_check_len() {
let reg = regorus_alias_registry_new();
let json = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1);
regorus_result_drop(r);
regorus_alias_registry_drop(reg);
}
#[test]
fn load_manifest_and_check_len() {
let reg = regorus_alias_registry_new();
let json = c(MANIFEST);
let r = regorus_alias_registry_load_manifest(reg, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1);
regorus_result_drop(r);
regorus_alias_registry_drop(reg);
}
#[test]
fn load_invalid_json_returns_error() {
let reg = regorus_alias_registry_new();
let bad = c("not valid json");
let r = regorus_alias_registry_load_json(reg, bad.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
regorus_alias_registry_drop(reg);
}
#[test]
fn normalize_and_wrap_round_trip() {
let reg = regorus_alias_registry_new();
let aliases = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let resource = c(r#"{
"name": "acct1",
"type": "Microsoft.Storage/storageAccounts",
"properties": { "supportsHttpsTrafficOnly": true }
}"#);
let api = c("2023-01-01");
let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#);
let params = c(r#"{"env": "prod"}"#);
// Normalize
let r = regorus_alias_registry_normalize_and_wrap(
reg,
resource.as_ptr(),
api.as_ptr(),
ctx.as_ptr(),
params.as_ptr(),
);
let envelope_json = assert_ok_string(&r);
regorus_result_drop(r);
// Parse and verify structure
let envelope: serde_json::Value =
serde_json::from_str(&envelope_json).expect("invalid JSON output");
assert!(
envelope.get("resource").is_some(),
"envelope missing 'resource'"
);
assert!(
envelope.get("parameters").is_some(),
"envelope missing 'parameters'"
);
assert!(
envelope.get("context").is_some(),
"envelope missing 'context'"
);
// The normalized resource should have lowercased alias fields
let res = &envelope["resource"];
assert_eq!(res["supportshttpstrafficonly"], true);
assert_eq!(res["name"], "acct1");
// Context and parameters should be passed through
assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1");
assert_eq!(envelope["parameters"]["env"], "prod");
// Denormalize the resource portion
let resource_json = serde_json::to_string(&res).expect("serialize resource");
let norm_cstr = c(&resource_json);
let r = regorus_alias_registry_denormalize(reg, norm_cstr.as_ptr(), api.as_ptr());
let denorm_json = assert_ok_string(&r);
regorus_result_drop(r);
let denorm: serde_json::Value =
serde_json::from_str(&denorm_json).expect("invalid denorm JSON");
// Should be back under properties with restored casing
assert_eq!(
denorm["properties"]["supportsHttpsTrafficOnly"], true,
"expected restored casing under properties"
);
regorus_alias_registry_drop(reg);
}
#[test]
fn denormalize_invalid_json_returns_error() {
let reg = regorus_alias_registry_new();
let aliases = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let bad = c("not json");
let api = c("2023-01-01");
let r = regorus_alias_registry_denormalize(reg, bad.as_ptr(), api.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
regorus_alias_registry_drop(reg);
}
#[test]
fn normalize_data_plane_manifest() {
let reg = regorus_alias_registry_new();
let manifest = c(MANIFEST);
let r = regorus_alias_registry_load_manifest(reg, manifest.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let resource = c(r#"{
"type": "Microsoft.KeyVault.Data/vaults/certificates",
"keySize": 2048
}"#);
let api = c("7.0");
let ctx = c("{}");
let params = c("{}");
let r = regorus_alias_registry_normalize_and_wrap(
reg,
resource.as_ptr(),
api.as_ptr(),
ctx.as_ptr(),
params.as_ptr(),
);
let envelope_json = assert_ok_string(&r);
regorus_result_drop(r);
let envelope: serde_json::Value =
serde_json::from_str(&envelope_json).expect("invalid JSON output");
assert_eq!(envelope["resource"]["keysize"], 2048);
regorus_alias_registry_drop(reg);
}
#[test]
fn empty_registry_normalize() {
let reg = regorus_alias_registry_new();
let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#);
let api = c("");
let ctx = c("{}");
let params = c("{}");
let r = regorus_alias_registry_normalize_and_wrap(
reg,
resource.as_ptr(),
api.as_ptr(),
ctx.as_ptr(),
params.as_ptr(),
);
let json = assert_ok_string(&r);
regorus_result_drop(r);
let envelope: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
// Without aliases, properties should still be flattened
assert_eq!(envelope["resource"]["foo"], 1);
assert_eq!(envelope["resource"]["name"], "test");
regorus_alias_registry_drop(reg);
}
}

View File

@@ -1,14 +1,17 @@
// 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`.
#[derive(Debug, PartialEq)]
#[repr(C)]
pub enum RegorusStatus {
/// The operation was successful.
@@ -34,6 +37,12 @@ pub enum RegorusStatus {
/// Invalid policy content.
InvalidPolicy,
/// The engine panicked and cannot be reused until reset.
Panic,
/// The engine remains poisoned because a previous panic was detected.
Poisoned,
}
/// Type of data contained in RegorusResult
@@ -84,6 +93,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 {
@@ -179,6 +201,18 @@ impl RegorusResult {
}
}
impl RegorusBuffer {
pub(crate) fn from_vec(mut data: Vec<u8>) -> *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(),
@@ -216,6 +250,21 @@ pub(crate) fn to_regorus_string_result(r: Result<String>) -> 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.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::format;
use alloc::vec::Vec;
@@ -43,55 +44,54 @@ pub extern "C" fn regorus_compile_policy_with_entrypoint(
modules_len: usize,
entry_point_rule: *const c_char,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
with_unwind_guard(|| {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
let entry_rule = match from_c_str(entry_point_rule) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidEntrypoint,
format!("Invalid entry point rule string: {e}"),
)
}
};
let entry_rule = match from_c_str(entry_point_rule) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidEntrypoint,
format!("Invalid entry point rule string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_with_entrypoint(data, &policy_modules, entry_rule.into()) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match compile_policy_with_entrypoint(data, &policy_modules, entry_rule.into()) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Policy compilation failed: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Policy compilation failed: {e}"),
),
}
})
}
/// Compiles a target-aware policy from data and modules.
@@ -122,45 +122,44 @@ pub extern "C" fn regorus_compile_policy_for_target(
modules: *const RegorusPolicyModule,
modules_len: usize,
) -> RegorusResult {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
with_unwind_guard(|| {
let data_str = match from_c_str(data_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid data JSON string: {e}"),
)
}
};
// Parse data JSON
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
let data = match Value::from_json_str(&data_str) {
Ok(data) => data,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse data JSON: {e}"),
)
}
};
// Convert C modules array to Rust Vec
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
let policy_modules = match convert_c_modules_to_rust(modules, modules_len) {
Ok(modules) => modules,
Err(status) => return RegorusResult::err(status),
};
// Call the convenience function
match compile_policy_for_target(data, &policy_modules) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match compile_policy_for_target(data, &policy_modules) {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Target-aware policy compilation failed: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Target-aware policy compilation failed: {e}"),
),
}
})
}
/// Helper function to convert C module array to Rust Vec<PolicyModule>.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
use crate::common::*;
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
use alloc::string::String;
use anyhow::Result;
@@ -35,20 +36,23 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
compiled_policy: *mut RegorusCompiledPolicy,
input: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)?
.compiled_policy
.eval_with_input(input_value)?;
result.to_json_str()
}();
with_unwind_guard(|| {
let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)?
.compiled_policy
.eval_with_input(input_value)?;
result.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Configure the execution timer for evaluations of this compiled policy.
/// Get information about the compiled policy including metadata about modules,
/// target configuration, and resource types.
///
@@ -59,14 +63,16 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
pub extern "C" fn regorus_compiled_policy_get_policy_info(
compiled_policy: *mut RegorusCompiledPolicy,
) -> RegorusResult {
let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}();
with_unwind_guard(|| {
let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}

View File

@@ -8,6 +8,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
@@ -29,45 +30,45 @@ pub extern "C" fn regorus_effect_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid effect schema JSON string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid effect schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse effect schema JSON: {e}"),
)
}
};
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse effect schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::effect::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register effect schema: {e}"),
),
}
match schemas::effect::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register effect schema: {e}"),
),
}
})
}
/// Check if an effect schema with the given name exists.
@@ -83,18 +84,20 @@ pub extern "C" fn regorus_effect_schema_register(
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let contains = schemas::effect::contains(&schema_name);
RegorusResult::ok_bool(contains)
let contains = schemas::effect::contains(&schema_name);
RegorusResult::ok_bool(contains)
})
}
/// Get the number of registered effect schemas.
@@ -104,8 +107,10 @@ pub extern "C" fn regorus_effect_schema_contains(name: *const c_char) -> Regorus
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_len() -> RegorusResult {
let count = schemas::effect::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = schemas::effect::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the effect schema registry is empty.
@@ -115,8 +120,10 @@ pub extern "C" fn regorus_effect_schema_len() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_is_empty() -> RegorusResult {
let is_empty = schemas::effect::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = schemas::effect::is_empty();
RegorusResult::ok_bool(is_empty)
})
}
/// List all registered effect schema names as a JSON array.
@@ -126,14 +133,16 @@ pub extern "C" fn regorus_effect_schema_is_empty() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_list_names() -> RegorusResult {
let names = schemas::effect::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize effect schema names to JSON: {e}"),
),
}
with_unwind_guard(|| {
let names = schemas::effect::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize effect schema names to JSON: {e}"),
),
}
})
}
/// Remove an effect schema by name.
@@ -149,18 +158,20 @@ pub extern "C" fn regorus_effect_schema_list_names() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid effect schema name string: {e}"),
)
}
};
let removed = schemas::effect::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
let removed = schemas::effect::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
})
}
/// Clear all effect schemas from the registry.
@@ -170,6 +181,8 @@ pub extern "C" fn regorus_effect_schema_remove(name: *const c_char) -> RegorusRe
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_effect_schema_clear() -> RegorusResult {
schemas::effect::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
with_unwind_guard(|| {
schemas::effect::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
})
}

View File

@@ -5,13 +5,23 @@ use crate::common::{
from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus,
};
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig;
use crate::limits::RegorusPolicyLengthConfig;
use crate::lock::{new_handle, read, 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;
#[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 {
@@ -67,6 +77,101 @@ mod tests {
}
}
#[cfg(all(test, feature = "std"))]
mod panic_tests {
use super::{
regorus_engine_drop, regorus_engine_eval_query, regorus_engine_get_policies,
regorus_engine_new,
};
use crate::common::{regorus_result_drop, RegorusStatus};
use crate::panic_guard::{is_poisoned, reset_poison};
use alloc::boxed::Box;
use regorus::Value;
use std::ffi::{CStr, CString};
#[test]
fn catches_extension_panics_and_marks_poison() {
reset_poison();
let engine_ptr = regorus_engine_new();
assert!(!engine_ptr.is_null(), "engine allocation must succeed");
assert!(!is_poisoned(), "guard must start unpoisoned");
unsafe {
let engine = &mut *engine_ptr;
{
let mut guard = engine
.try_write()
.expect("exclusive access to configure engine");
guard
.add_extension(
"panic_extension".to_string(),
0,
Box::new(|_| -> anyhow::Result<Value> { panic!("ffi extension panic") }),
)
.expect("extension registration must succeed");
guard
.add_policy(
"panic.rego".to_string(),
"package panic\n\ndefault allow = false\n\nallow if {\n panic_extension()\n}"
.to_string(),
)
.expect("policy registration must succeed");
}
}
let query = CString::new("data.panic.allow").expect("valid query string");
let panic_result = regorus_engine_eval_query(engine_ptr, query.as_ptr());
assert!(matches!(panic_result.status, RegorusStatus::Panic));
unsafe {
assert!(
!panic_result.error_message.is_null(),
"panic details must be present"
);
let message = CStr::from_ptr(panic_result.error_message)
.to_str()
.expect("error message utf8");
assert!(
message.contains("ffi extension panic"),
"panic payload must bubble across guard"
);
}
regorus_result_drop(panic_result);
assert!(is_poisoned(), "engine must be marked poisoned after panic");
let poisoned_result = regorus_engine_get_policies(engine_ptr);
assert!(matches!(poisoned_result.status, RegorusStatus::Poisoned));
unsafe {
assert!(
!poisoned_result.error_message.is_null(),
"poison message must be present"
);
let message = CStr::from_ptr(poisoned_result.error_message)
.to_str()
.expect("poison message utf8");
assert!(
message.contains("regorus is poisoned"),
"poison message must inform callers"
);
}
regorus_result_drop(poisoned_result);
regorus_engine_drop(engine_ptr);
reset_poison();
}
}
#[no_mangle]
#[cfg(feature = "std")]
pub extern "C" fn regorus_engine_test_trigger_panic() -> RegorusResult {
with_unwind_guard(|| panic!("regorus ffi test panic"))
}
#[no_mangle]
pub extern "C" fn regorus_engine_test_reset_poison() {
crate::panic_guard::reset_poison();
}
#[no_mangle]
/// Construct a new Engine
///
@@ -116,11 +221,13 @@ pub extern "C" fn regorus_engine_add_policy(
path: *const c_char,
rego: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy(from_c_str(path)?, from_c_str(rego)?)
}())
})
}
#[cfg(feature = "std")]
@@ -129,11 +236,13 @@ pub extern "C" fn regorus_engine_add_policy_from_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy_from_file(from_c_str(path)?)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_policy_from_file(from_c_str(path)?)
}())
})
}
/// Add policy data.
@@ -145,11 +254,13 @@ pub extern "C" fn regorus_engine_add_data_json(
engine: *mut RegorusEngine,
data: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}())
})
}
/// Get list of loaded Rego packages as JSON.
@@ -157,11 +268,13 @@ pub extern "C" fn regorus_engine_add_data_json(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
#[no_mangle]
pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg)
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg)
}())
})
}
/// Get list of policies as JSON.
@@ -169,11 +282,13 @@ pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> Reg
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies
#[no_mangle]
pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_policies_as_json()
}())
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_policies_as_json()
}())
})
}
#[cfg(feature = "std")]
@@ -182,11 +297,13 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}())
})
}
/// Clear policy data.
@@ -194,12 +311,14 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data
#[no_mangle]
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_data();
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_data();
Ok(())
}())
})
}
/// Set input.
@@ -211,12 +330,14 @@ pub extern "C" fn regorus_engine_set_input_json(
engine: *mut RegorusEngine,
input: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(())
}())
})
}
#[cfg(feature = "std")]
@@ -225,12 +346,14 @@ pub extern "C" fn regorus_engine_set_input_from_json_file(
engine: *mut RegorusEngine,
path: *const c_char,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(())
}())
})
}
/// Evaluate query.
@@ -242,16 +365,18 @@ pub extern "C" fn regorus_engine_eval_query(
engine: *mut RegorusEngine,
query: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let results = guard.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let results = guard.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Evaluate specified rule.
@@ -263,15 +388,17 @@ pub extern "C" fn regorus_engine_eval_rule(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.eval_rule(from_c_str(rule)?)?.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.eval_rule(from_c_str(rule)?)?.to_json_str()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable coverage.
@@ -284,12 +411,14 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable);
Ok(())
}())
})
}
/// Get coverage report.
@@ -298,15 +427,17 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable strict builtin errors.
@@ -317,15 +448,81 @@ pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine)
pub extern "C" fn regorus_engine_set_strict_builtin_errors(
engine: *mut RegorusEngine,
strict: bool,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_strict_builtin_errors(strict);
Ok(())
}())
})
}
#[no_mangle]
/// Configure the execution timer for a specific engine instance.
pub extern "C" fn regorus_engine_set_execution_timer_config(
engine: *mut RegorusEngine,
config: *const RegorusExecutionTimerConfig,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let config = unsafe {
config
.as_ref()
.copied()
.ok_or_else(|| anyhow!("execution timer config pointer is null"))?
};
let mut guard = engine.try_write()?;
guard.set_execution_timer_config(config.to_execution_timer_config()?);
Ok(())
}())
}
#[no_mangle]
/// Clear the engine-specific execution timer configuration.
pub extern "C" fn regorus_engine_clear_execution_timer_config(
engine: *mut RegorusEngine,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_strict_builtin_errors(strict);
guard.clear_execution_timer_config();
Ok(())
}())
}
/// Set the policy length limits used when loading policies.
#[no_mangle]
pub extern "C" fn regorus_engine_set_policy_length_config(
engine: *mut RegorusEngine,
config: RegorusPolicyLengthConfig,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_policy_length_config(config.to_policy_length_config()?);
Ok(())
}())
})
}
/// Clear the policy length configuration, reverting to defaults.
#[no_mangle]
pub extern "C" fn regorus_engine_clear_policy_length_config(
engine: *mut RegorusEngine,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_policy_length_config();
Ok(())
}())
})
}
/// Get pretty printed coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty
@@ -334,15 +531,17 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors(
pub extern "C" fn regorus_engine_get_coverage_report_pretty(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Clear coverage data.
@@ -351,12 +550,14 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty(
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_coverage_data();
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_coverage_data();
Ok(())
}())
})
}
/// Whether to gather output of print statements.
@@ -368,12 +569,14 @@ pub extern "C" fn regorus_engine_set_gather_prints(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_gather_prints(enable);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_gather_prints(enable);
Ok(())
}())
})
}
/// Take all the gathered print statements.
@@ -381,15 +584,17 @@ pub extern "C" fn regorus_engine_set_gather_prints(
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints
#[no_mangle]
pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
Ok(serde_json::to_string_pretty(&guard.take_prints()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
Ok(serde_json::to_string_pretty(&guard.take_prints()?)?)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Get AST of policies.
@@ -398,15 +603,17 @@ pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> Rego
#[no_mangle]
#[cfg(feature = "ast")]
pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_ast_as_json()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
guard.get_ast_as_json()
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Gets the package names defined in each policy added to the engine.
@@ -417,15 +624,18 @@ pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) ->
pub extern "C" fn regorus_engine_get_policy_package_names(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_package_names()?).map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_package_names()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Gets the parameters defined in each policy added to the engine.
@@ -436,15 +646,18 @@ pub extern "C" fn regorus_engine_get_policy_package_names(
pub extern "C" fn regorus_engine_get_policy_parameters(
engine: *mut RegorusEngine,
) -> RegorusResult {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_parameters()?).map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<String> {
let engine = to_ref(engine)?;
let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_parameters()?)
.map_err(anyhow::Error::msg)
}();
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Enable/disable rego v1.
@@ -455,16 +668,18 @@ pub extern "C" fn regorus_engine_set_rego_v0(
engine: *mut RegorusEngine,
enable: bool,
) -> RegorusResult {
let output = || -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => to_regorus_result(Err(e)),
}
with_unwind_guard(|| {
let output = || -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_rego_v0(enable);
Ok(())
}();
match output {
Ok(()) => RegorusResult::ok_void(),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Compile a target-aware policy from the current engine state.
@@ -476,37 +691,39 @@ pub extern "C" fn regorus_engine_set_rego_v0(
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult {
let engine = match to_ref(engine) {
Ok(engine) => engine,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Failed to get engine reference: {e}"),
)
}
};
with_unwind_guard(|| {
let engine = match to_ref(engine) {
Ok(engine) => engine,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Failed to get engine reference: {e}"),
)
}
};
let mut guard = match engine.try_write() {
Ok(guard) => guard,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to lock engine: {e}"),
)
}
};
let mut guard = match engine.try_write() {
Ok(guard) => guard,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to lock engine: {e}"),
)
}
};
match guard.compile_for_target() {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match guard.compile_for_target() {
Ok(compiled_policy) => {
let wrapped_policy = RegorusCompiledPolicy { compiled_policy };
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile for target: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile for target: {e}"),
),
}
})
}
/// Compile a policy with a specific entry point rule.
@@ -520,23 +737,87 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint(
engine: *mut RegorusEngine,
rule: *const c_char,
) -> RegorusResult {
let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into();
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy })
}();
with_unwind_guard(|| {
let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into();
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy })
}();
match result {
Ok(wrapped_policy) => {
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
match result {
Ok(wrapped_policy) => {
let boxed_policy = Box::new(wrapped_policy);
RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void)
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile with entrypoint: {e}"),
),
}
Err(e) => RegorusResult::err_with_message(
RegorusStatus::CompilationFailed,
format!("Failed to compile with entrypoint: {e}"),
),
}
})
}
/// 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<Arc<Program>> {
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<str> = (*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}"),
),
}
})
}

View File

@@ -5,12 +5,19 @@
extern crate alloc;
mod alias_registry;
mod allocator;
mod common;
mod compile;
mod compiled_policy;
mod effect_registry;
mod engine;
mod limits;
mod lock;
mod panic_guard;
#[cfg(feature = "rbac")]
mod rbac;
#[cfg(feature = "rvm")]
pub(crate) mod rvm;
mod schema_registry;
mod target_registry;

276
bindings/ffi/src/limits.rs Normal file
View File

@@ -0,0 +1,276 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{to_regorus_result, RegorusResult, RegorusStatus};
use alloc::format;
use anyhow::{anyhow, Result};
use core::num::{NonZeroU32, NonZeroUsize};
use core::time::Duration;
use regorus::utils::limits::{self, ExecutionTimerConfig};
#[cfg(feature = "allocator-memory-limits")]
fn some_or_none(flag: bool, value: u64) -> Option<u64> {
if flag {
Some(value)
} else {
None
}
}
#[cfg(feature = "allocator-memory-limits")]
fn optional_u64_to_result(value: Option<u64>) -> RegorusResult {
match value {
Some(bytes) => {
if bytes > i64::MAX as u64 {
RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!(
"value {bytes} exceeds i64::MAX ({max}) bridge limit",
max = i64::MAX
),
)
} else {
let mut result = RegorusResult::ok_int(bytes as i64);
result.bool_value = true;
result
}
}
None => {
let mut result = RegorusResult::ok_void();
result.bool_value = false;
result
}
}
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_set_global_memory_limit(limit: u64, has_limit: bool) -> RegorusResult {
::regorus::set_global_memory_limit(some_or_none(has_limit, limit));
RegorusResult::ok_void()
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_set_global_memory_limit(_limit: u64, _has_limit: bool) -> RegorusResult {
feature_disabled("regorus_set_global_memory_limit")
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_get_global_memory_limit() -> RegorusResult {
optional_u64_to_result(::regorus::global_memory_limit())
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_get_global_memory_limit() -> RegorusResult {
feature_disabled("regorus_get_global_memory_limit")
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_check_global_memory_limit() -> RegorusResult {
match ::regorus::check_global_memory_limit() {
Ok(()) => RegorusResult::ok_void(),
Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")),
}
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_check_global_memory_limit() -> RegorusResult {
feature_disabled("regorus_check_global_memory_limit")
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_flush_thread_memory_counters() -> RegorusResult {
::regorus::flush_thread_memory_counters();
RegorusResult::ok_void()
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_flush_thread_memory_counters() -> RegorusResult {
feature_disabled("regorus_flush_thread_memory_counters")
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_set_thread_flush_threshold_override(
bytes: u64,
has_threshold: bool,
) -> RegorusResult {
::regorus::set_thread_flush_threshold_override(some_or_none(has_threshold, bytes));
RegorusResult::ok_void()
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_set_thread_flush_threshold_override(
_bytes: u64,
_has_threshold: bool,
) -> RegorusResult {
feature_disabled("regorus_set_thread_flush_threshold_override")
}
#[cfg(feature = "allocator-memory-limits")]
#[no_mangle]
pub extern "C" fn regorus_get_thread_memory_flush_threshold() -> RegorusResult {
optional_u64_to_result(::regorus::thread_memory_flush_threshold())
}
#[cfg(not(feature = "allocator-memory-limits"))]
#[no_mangle]
pub extern "C" fn regorus_get_thread_memory_flush_threshold() -> RegorusResult {
feature_disabled("regorus_get_thread_memory_flush_threshold")
}
#[cfg(not(feature = "allocator-memory-limits"))]
fn feature_disabled(function: &str) -> RegorusResult {
RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("{function} unavailable: regorus built without allocator-memory-limits feature"),
)
}
/// FFI representation of [`ExecutionTimerConfig`].
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RegorusExecutionTimerConfig {
/// Wall-clock limit expressed in nanoseconds.
pub limit_ns: u64,
/// Number of work units between timer checks (must be non-zero).
pub check_interval: u32,
}
impl RegorusExecutionTimerConfig {
pub fn to_execution_timer_config(self) -> Result<ExecutionTimerConfig> {
let check_interval = NonZeroU32::new(self.check_interval)
.ok_or_else(|| anyhow!("execution_timer.check_interval must be non-zero"))?;
let limit = Duration::from_nanos(self.limit_ns);
Ok(ExecutionTimerConfig {
limit,
check_interval,
})
}
}
/// FFI representation of [`regorus::PolicyLengthConfig`].
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RegorusPolicyLengthConfig {
/// Maximum column width per line (must be non-zero).
pub max_col: u32,
/// Maximum policy file size in bytes (must be non-zero).
pub max_file_bytes: usize,
/// Maximum number of lines per policy file (must be non-zero).
pub max_lines: usize,
}
impl RegorusPolicyLengthConfig {
pub fn to_policy_length_config(self) -> Result<regorus::PolicyLengthConfig> {
Ok(regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(self.max_col)
.ok_or_else(|| anyhow!("max_col must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(self.max_file_bytes)
.ok_or_else(|| anyhow!("max_file_bytes must be non-zero"))?,
max_lines: NonZeroUsize::new(self.max_lines)
.ok_or_else(|| anyhow!("max_lines must be non-zero"))?,
})
}
}
#[no_mangle]
pub extern "C" fn regorus_set_fallback_execution_timer_config(
config: RegorusExecutionTimerConfig,
) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
limits::set_fallback_execution_timer_config(Some(config.to_execution_timer_config()?));
Ok(())
}())
}
#[no_mangle]
pub extern "C" fn regorus_clear_fallback_execution_timer_config() -> RegorusResult {
limits::set_fallback_execution_timer_config(None);
RegorusResult::ok_void()
}
// ---------------------------------------------------------------------------
// Cache configuration (global)
// ---------------------------------------------------------------------------
/// FFI representation of [`regorus::cache::Config`].
#[cfg(feature = "cache")]
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RegorusCacheConfig {
/// Maximum compiled regex patterns (default 256, 0 = disabled).
pub regex: usize,
/// Maximum compiled glob matchers (default 128, 0 = disabled).
pub glob: usize,
}
/// Configure the global pattern caches used by `regex.*` and `glob.*` builtins.
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "C" fn regorus_set_cache_config(config: RegorusCacheConfig) -> RegorusResult {
regorus::cache::configure(regorus::cache::Config {
regex: config.regex,
glob: config.glob,
});
RegorusResult::ok_void()
}
/// Clear all entries from every pattern cache.
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "C" fn regorus_clear_cache() -> RegorusResult {
regorus::cache::clear();
RegorusResult::ok_void()
}
#[cfg(test)]
mod tests {
use super::{
optional_u64_to_result, regorus_get_global_memory_limit, regorus_set_global_memory_limit,
};
use crate::common::{regorus_result_drop, RegorusDataType, RegorusStatus};
#[test]
fn optional_some_returns_integer() {
let result = optional_u64_to_result(Some(123));
assert!(result.bool_value);
assert!(matches!(result.data_type, RegorusDataType::Integer));
assert_eq!(result.int_value, 123);
}
#[test]
fn optional_none_returns_void() {
let result = optional_u64_to_result(None);
assert!(!result.bool_value);
assert!(matches!(result.data_type, RegorusDataType::None));
assert_eq!(result.int_value, 0);
}
#[test]
fn ffi_roundtrips_global_limit() {
let limit = 456_u64;
let result = regorus_set_global_memory_limit(limit, true);
assert!(matches!(result.status, RegorusStatus::Ok));
regorus_result_drop(result);
let result = regorus_get_global_memory_limit();
assert!(matches!(result.status, RegorusStatus::Ok));
assert!(result.bool_value);
assert!(matches!(result.data_type, RegorusDataType::Integer));
assert_eq!(result.int_value, 456);
regorus_result_drop(result);
let result = regorus_set_global_memory_limit(0, false);
regorus_result_drop(result);
}
}

View File

@@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Minimal helpers for catching panics inside the FFI layer.
//!
//! These are not yet wired into the exported functions; they will
//! be used once the integration work is complete.
extern crate alloc;
use crate::common::{RegorusResult, RegorusStatus};
use alloc::string::String;
use core::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "std")]
use std::{
backtrace::Backtrace,
cell::RefCell,
panic::{self, AssertUnwindSafe},
};
#[cfg(feature = "std")]
thread_local! {
// Stashes the formatted panic + backtrace for whichever call last panicked on this thread.
static PANIC_BACKTRACE: RefCell<Option<String>> = const { RefCell::new(None) };
}
#[cfg(feature = "std")]
type PanicHook = dyn Fn(&panic::PanicHookInfo<'_>) + Sync + Send + 'static;
#[cfg(feature = "std")]
/// RAII helper that installs a per-call panic hook and restores the prior hook on drop.
struct PanicHookGuard {
previous: Option<Box<PanicHook>>,
}
#[cfg(feature = "std")]
impl PanicHookGuard {
fn install() -> Self {
// Remember whatever hook the embedding application already registered.
let previous = panic::take_hook();
PANIC_BACKTRACE.with(|slot| {
slot.replace(None);
});
// Install our temporary hook so we can capture a backtrace for this invocation.
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::force_capture();
PANIC_BACKTRACE.with(|slot| {
slot.replace(Some(format!(
"panic hook observed: {}\nbacktrace:\n{:#?}",
info, backtrace
)));
});
}));
Self {
previous: Some(previous),
}
}
}
#[cfg(feature = "std")]
impl Drop for PanicHookGuard {
fn drop(&mut self) {
if let Some(previous) = self.previous.take() {
// Restore the original panic hook before we return control to the host.
panic::set_hook(previous);
}
}
}
static POISONED: AtomicBool = AtomicBool::new(false);
/// Result of attempting to run `f` while guarding against unwinding.
pub(crate) enum GuardResult<T> {
/// Closure completed successfully.
Success(T),
/// Closure panicked; contains a best-effort string payload.
Panic(String),
}
pub(crate) fn with_unwind_guard<F>(f: F) -> RegorusResult
where
F: FnOnce() -> RegorusResult,
{
if is_poisoned() {
return poisoned_result();
}
// The closure passed across this boundary closes over raw pointers and lock guards.
// These types are not unwind safe by default and may become poisoned if a panic occurs.
// We therefore use AssertUnwindSafe to get the compiler to accept the closure.
// Upon unwind, we mark regorus as poisoned and disallow further use.
#[cfg(feature = "std")]
{
let outcome = {
let _hook_guard = PanicHookGuard::install();
match panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => GuardResult::Success(value),
Err(payload) => GuardResult::Panic(panic_message_to_string(payload)),
}
};
finalize(outcome)
}
#[cfg(not(feature = "std"))]
return finalize(GuardResult::Success(f()));
}
fn finalize(outcome: GuardResult<RegorusResult>) -> RegorusResult {
match outcome {
GuardResult::Success(result) => result,
GuardResult::Panic(message) => {
trip_poison();
RegorusResult::err_with_message(RegorusStatus::Panic, message)
}
}
}
#[cfg(feature = "std")]
fn panic_message_to_string(payload: Box<dyn core::any::Any + Send + 'static>) -> String {
let mut message = if let Some(s) = payload.downcast_ref::<&str>() {
(*s).into()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
String::from("regorus encountered panic")
};
if let Some(backtrace) = take_panic_backtrace() {
message.push('\n');
message.push_str(&backtrace);
}
message
}
#[cfg(feature = "std")]
fn take_panic_backtrace() -> Option<String> {
PANIC_BACKTRACE.with(|slot| slot.borrow_mut().take())
}
fn poisoned_result() -> RegorusResult {
RegorusResult::err_with_message(
RegorusStatus::Poisoned,
String::from("regorus is poisoned after a previous panic"),
)
}
fn trip_poison() {
POISONED.store(true, Ordering::Release);
}
pub(crate) fn is_poisoned() -> bool {
POISONED.load(Ordering::Acquire)
}
pub(crate) fn reset_poison() {
POISONED.store(false, Ordering::Release);
}

59
bindings/ffi/src/rbac.rs Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use alloc::format;
use core::ffi::c_char;
use regorus::languages::azure_rbac::ast::EvaluationContext;
use regorus::languages::azure_rbac::interpreter::ConditionInterpreter;
#[no_mangle]
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
///
/// * `condition`: RBAC condition string.
/// * `context_json`: JSON representation of EvaluationContext.
pub extern "C" fn regorus_rbac_engine_eval_condition(
condition: *const c_char,
context_json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
let condition = match from_c_str(condition) {
Ok(value) => value,
Err(err) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("{err}"),
)
}
};
let context_json = match from_c_str(context_json) {
Ok(value) => value,
Err(err) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("{err}"),
)
}
};
let context: EvaluationContext = match serde_json::from_str(&context_json) {
Ok(context) => context,
Err(err) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("invalid context json: {err}"),
)
}
};
let interpreter = ConditionInterpreter::new(&context);
match interpreter.evaluate_str(&condition) {
Ok(result) => RegorusResult::ok_bool(result),
Err(err) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("condition evaluation failed: {err}"),
),
}
})
}

606
bindings/ffi/src/rvm.rs Normal file
View File

@@ -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<Program>,
}
/// Wrapper for `regorus::rvm::RegoVM`.
pub struct RegorusRvm {
vm: Handle<RegoVM>,
}
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<WriteGuard<'_, RegoVM>> {
try_write(&self.vm).ok_or_else(Self::contention_error)
}
fn try_read(&self) -> Result<ReadGuard<'_, RegoVM>> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<Vec<String>> {
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<Vec<PolicyModule>> {
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)
}

View File

@@ -9,6 +9,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard;
use regorus::{registry::schemas, Schema};
use std::os::raw::c_char;
@@ -32,45 +33,45 @@ pub extern "C" fn regorus_resource_schema_register(
name: *const c_char,
schema_json: *const c_char,
) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid schema JSON string: {e}"),
)
}
};
let schema_str = match from_c_str(schema_json) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Invalid schema JSON string: {e}"),
)
}
};
// Parse schema from JSON
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse schema JSON: {e}"),
)
}
};
let schema = match Schema::from_json_str(&schema_str) {
Ok(schema) => schema,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidDataFormat,
format!("Failed to parse schema JSON: {e}"),
)
}
};
// Register the schema
match schemas::resource::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register schema: {e}"),
),
}
match schemas::resource::register(schema_name, schema.into()) {
Ok(()) => RegorusResult::ok_pointer(std::ptr::null_mut()),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to register schema: {e}"),
),
}
})
}
/// Check if a resource schema with the given name exists.
@@ -86,18 +87,20 @@ pub extern "C" fn regorus_resource_schema_register(
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_contains(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let contains = schemas::resource::contains(&schema_name);
RegorusResult::ok_bool(contains)
let contains = schemas::resource::contains(&schema_name);
RegorusResult::ok_bool(contains)
})
}
/// Get the number of registered resource schemas.
@@ -107,8 +110,10 @@ pub extern "C" fn regorus_resource_schema_contains(name: *const c_char) -> Regor
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_len() -> RegorusResult {
let count = schemas::resource::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = schemas::resource::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the resource schema registry is empty.
@@ -118,8 +123,10 @@ pub extern "C" fn regorus_resource_schema_len() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_is_empty() -> RegorusResult {
let is_empty = schemas::resource::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = schemas::resource::is_empty();
RegorusResult::ok_bool(is_empty)
})
}
/// List all registered resource schema names as a JSON array.
@@ -129,14 +136,16 @@ pub extern "C" fn regorus_resource_schema_is_empty() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_list_names() -> RegorusResult {
let names = schemas::resource::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize schema names to JSON: {e}"),
),
}
with_unwind_guard(|| {
let names = schemas::resource::list_names();
match serde_json::to_string(&names) {
Ok(json_str) => RegorusResult::ok_string(json_str),
Err(e) => RegorusResult::err_with_message(
RegorusStatus::Error,
format!("Failed to serialize schema names to JSON: {e}"),
),
}
})
}
/// Remove a resource schema by name.
@@ -152,18 +161,20 @@ pub extern "C" fn regorus_resource_schema_list_names() -> RegorusResult {
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_remove(name: *const c_char) -> RegorusResult {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
with_unwind_guard(|| {
let schema_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid schema name string: {e}"),
)
}
};
let removed = schemas::resource::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
let removed = schemas::resource::remove(&schema_name).is_some();
RegorusResult::ok_bool(removed)
})
}
/// Clear all resource schemas from the registry.
@@ -173,6 +184,8 @@ pub extern "C" fn regorus_resource_schema_remove(name: *const c_char) -> Regorus
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_resource_schema_clear() -> RegorusResult {
schemas::resource::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
with_unwind_guard(|| {
schemas::resource::clear();
RegorusResult::ok_pointer(std::ptr::null_mut())
})
}

View File

@@ -4,6 +4,7 @@
#![cfg(feature = "azure_policy")]
use crate::common::*;
use crate::panic_guard::with_unwind_guard;
use anyhow::Result;
use std::os::raw::c_char;
@@ -16,12 +17,14 @@ use std::os::raw::c_char;
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_register_target_from_json(target_json: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let target_str = from_c_str(target_json)?;
let target = regorus::Target::from_json_str(&target_str)?;
regorus::registry::targets::register(regorus::Rc::new(target))?;
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let target_str = from_c_str(target_json)?;
let target = regorus::Target::from_json_str(&target_str)?;
regorus::registry::targets::register(regorus::Rc::new(target))?;
Ok(())
}())
})
}
/// Check if a target is registered.
@@ -36,31 +39,35 @@ pub extern "C" fn regorus_register_target_from_json(target_json: *const c_char)
/// The name parameter must be a valid null-terminated UTF-8 string.
#[no_mangle]
pub extern "C" fn regorus_target_registry_contains(name: *const c_char) -> RegorusResult {
let target_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid target name string: {e}"),
)
}
};
with_unwind_guard(|| {
let target_name = match from_c_str(name) {
Ok(s) => s,
Err(e) => {
return RegorusResult::err_with_message(
RegorusStatus::InvalidArgument,
format!("Invalid target name string: {e}"),
)
}
};
let contains = regorus::registry::targets::contains(&target_name);
RegorusResult::ok_bool(contains)
let contains = regorus::registry::targets::contains(&target_name);
RegorusResult::ok_bool(contains)
})
}
/// Get a list of all registered target names as JSON array.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_list_names() -> RegorusResult {
let names = regorus::registry::targets::list_names();
let output = serde_json::to_string_pretty(&names).map_err(anyhow::Error::msg);
with_unwind_guard(|| {
let names = regorus::registry::targets::list_names();
let output = serde_json::to_string_pretty(&names).map_err(anyhow::Error::msg);
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
match output {
Ok(out) => RegorusResult::ok_string(out),
Err(e) => to_regorus_result(Err(e)),
}
})
}
/// Remove a target from the registry by name.
@@ -69,19 +76,23 @@ pub extern "C" fn regorus_target_registry_list_names() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_remove(name: *const c_char) -> RegorusResult {
to_regorus_result(|| -> Result<()> {
let name_str = from_c_str(name)?;
regorus::registry::targets::remove(&name_str);
Ok(())
}())
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let name_str = from_c_str(name)?;
regorus::registry::targets::remove(&name_str);
Ok(())
}())
})
}
/// Clear all targets from the registry.
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_clear() -> RegorusResult {
regorus::registry::targets::clear();
RegorusResult::ok_void()
with_unwind_guard(|| {
regorus::registry::targets::clear();
RegorusResult::ok_void()
})
}
/// Get the number of registered targets.
@@ -91,8 +102,10 @@ pub extern "C" fn regorus_target_registry_clear() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_len() -> RegorusResult {
let count = regorus::registry::targets::len();
RegorusResult::ok_int(count as i64)
with_unwind_guard(|| {
let count = regorus::registry::targets::len();
RegorusResult::ok_int(count as i64)
})
}
/// Check if the target registry is empty.
@@ -102,6 +115,8 @@ pub extern "C" fn regorus_target_registry_len() -> RegorusResult {
#[no_mangle]
#[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_target_registry_is_empty() -> RegorusResult {
let is_empty = regorus::registry::targets::is_empty();
RegorusResult::ok_bool(is_empty)
with_unwind_guard(|| {
let is_empty = regorus::registry::targets::is_empty();
RegorusResult::ok_bool(is_empty)
})
}

View File

@@ -17,7 +17,15 @@ func main() {
engine := regorus.NewEngine()
defer engine.Close()
// Configure the global pattern caches.
if err = regorus.SetCacheConfig(regorus.CacheConfig{Regex: 256, Glob: 128}); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
engine.SetRegoV0(true)
// Raise the default col limit to 2000
engine.SetPolicyLengthConfig(regorus.PolicyLengthConfig{MaxCol: 2000, MaxFileBytes: 1048576, MaxLines: 20000})
elapsed1 := time.Since(t)
@@ -100,4 +108,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)
}

View File

@@ -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 (
@@ -214,3 +214,59 @@ func (e *Engine) TakePrints() (string, error) {
return C.GoString(result.output), nil
}
type PolicyLengthConfig struct {
MaxCol uint32
MaxFileBytes uint
MaxLines uint
}
func (e *Engine) SetPolicyLengthConfig(config PolicyLengthConfig) error {
c := C.RegorusPolicyLengthConfig{
max_col: C.uint32_t(config.MaxCol),
max_file_bytes: C.size_t(config.MaxFileBytes),
max_lines: C.size_t(config.MaxLines),
}
result := C.regorus_engine_set_policy_length_config(e.e, c)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) ClearPolicyLengthConfig() error {
result := C.regorus_engine_clear_policy_length_config(e.e)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
type CacheConfig struct {
Regex uint
Glob uint
}
func SetCacheConfig(config CacheConfig) error {
c := C.RegorusCacheConfig{
regex: C.size_t(config.Regex),
glob: C.size_t(config.Glob),
}
result := C.regorus_set_cache_config(c)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func ClearCache() error {
result := C.regorus_clear_cache()
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}

View File

@@ -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
}

View File

@@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show More