Compare commits

...

9 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 3f65685149 Update python binding to pyo3 0.29.0 2026-06-23 15:05:27 +00:00
copilot-swe-agent[bot] ae9cae8d22 Initial plan 2026-06-23 15:01:07 +00:00
Anand Krishnamoorthi ed6ae465b0 refactor(value): migrate Value::Object to Object storage abstraction (#736)
Builds on #57. Swap Value::Object's payload from Rc<BTreeMap<Value, Value>>
to Rc<Object> and migrate all call sites to the Object API.

as_object / as_object_mut keep their names but return &Object / &mut Object.
The mutable accessor handles Rc::make_mut internally, so callers no longer
do it themselves. Object grows into_value() and From<Object> for Value.
Value's serializer now delegates to Object::serialize, dropping a duplicate
non-string-key stringification path.

RVM IterationState::Object is rewritten around ObjectCursor: O(log n)
steps over a shared Rc<Object>, no eager pair snapshot. Snapshot
independence is preserved by Rc copy-on-write; setup_next_iteration
advances the cursor inline and advance() becomes a no-op for this variant.
A new iteration_state_object_is_snapshot_independent_of_source test
covers CoW against a mutated alias.

Value::Set still wraps Rc<BTreeSet<Value>>; the matching Set abstraction
and its swap ship in follow-up PRs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 18:04:37 -05:00
Mark Birger bd90453dd3 tests/opa: normalize path separators in folder filter on Windows (#742)
`run_opa_tests` builds `path_dir_str` from `path.strip_prefix(...).to_string_lossy()`,
which on Windows yields strings with backslash separators (e.g.
`v0\aggregates`). The folder filter then does an exact-string
comparison against the CLI arguments:

    let run_test = folders.is_empty()
        || folders.iter().any(|f| &path_dir_str == f);

CLI arguments use forward slashes (`v0/aggregates`), so on Windows
the comparison never matches, no tests are selected, and the function
bails with `"no matching tests found"`. This blocks the
`cargo xtask pre-push` hook for any Windows contributor.

Normalize `path_dir_str` to use forward slashes at construction
time. Reproduces before the fix as `cargo test ... --test opa --
v1/aggregates` exiting 1 with `no matching tests found`; after the
fix the same command runs 72 cases and the full hook command runs
2861 / 0 across 188 folders.

The duplicate platform check at the `is_rego_v0_test` site
(`path_dir_str.starts_with("v0/") || path_dir.starts_with("v0\\")`)
is left intact to keep the change minimal — the backslash branch
becomes redundant but is harmless.

Co-authored-by: Mark Birger <markbirger@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 16:00:24 -05:00
Anand Krishnamoorthi 11940ddb04 Introduce Object storage abstraction (#735)
Add an opaque Object type for the key→value storage backing
Value::Object. It exposes a small set of methods (get, insert, remove,
iter, iter_sorted, cursor, serde) and keeps the backing store private,
so future representations -- inline small-map, hash-backed, lazy,
arena, FFI-callback -- can plug in without touching the call sites
that name this type.

Nothing in the engine uses Object yet. Value::Object still wraps
Rc<BTreeMap<Value, Value>>; the payload swap and call-site migration
come in the next PR. Object stands on its own unit tests in the
meantime.

docs/value/object.md walks through the design, the precedents it
follows (serde_json::Map, toml::Table, simdjson DOM), and the
concrete workloads the abstraction is meant to unlock.

A matching Set abstraction follows in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-04 12:31:15 -05:00
Anand Krishnamoorthi 5b7010ba16 chore(rvm): add debug-mode invariant assertions (#737)
Encode VM stack/context/register lifecycle invariants as
debug_assert!s. Zero cost in release; surfaces violations during
debug-mode tests and CI.

Invariants covered:
- reset_execution_state postcondition: all stacks empty, registers
  resized to base and Undefined, rule_cache reset, pc/executed
  counters zeroed, builtins_cache cleared, execution_state Ready.
- Per-opcode invariant check (assert_vm_invariants) invoked at the
  top of run_stackless_loop and jump_to iterations: state is
  Ready/Running, registers non-empty, rule_cache sized to program,
  execution stack bounded by a debug-only sanity ceiling
  (DEBUG_MAX_EXECUTION_STACK_DEPTH = 4096; not a production limit).
- resume() precondition: execution_state is Suspended.
- execute_suspendable_entry precondition: clean state (callers reset
  immediately before).
- Rule finalize: call_rule_stack pop matches the finalized rule_index.
- IterationState::advance: Single iterator not advanced past
  consumption, Array index not at usize::MAX before saturating_add.

All assertions are gated by #[cfg(debug_assertions)] (directly or via
debug_assert!) so release builds are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-04 12:28:47 -05:00
dependabot[bot] ba7d29b134 build(deps): bump the rust-dependencies group across 5 directories with 5 updates (#734)
* build(deps): bump the rust-dependencies group across 5 directories with 5 updates

Bumps the rust-dependencies group with 4 updates in the / directory: [serde_json](https://github.com/serde-rs/json), spin, [dashmap](https://github.com/xacrimon/dashmap) and [toml_edit](https://github.com/toml-rs/toml).
Bumps the rust-dependencies group with 3 updates in the /bindings/ffi directory: [serde_json](https://github.com/serde-rs/json), spin and [dashmap](https://github.com/xacrimon/dashmap).
Bumps the rust-dependencies group with 2 updates in the /bindings/java directory: [serde_json](https://github.com/serde-rs/json) and spin.
Bumps the rust-dependencies group with 2 updates in the /bindings/python directory: [serde_json](https://github.com/serde-rs/json) and spin.
Bumps the rust-dependencies group with 3 updates in the /bindings/wasm directory: [serde_json](https://github.com/serde-rs/json), spin and [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen).


Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `dashmap` from 6.1.0 to 6.2.1
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v6.1.0...v6.2.1)

Updates `toml_edit` from 0.25.11+spec-1.1.0 to 0.25.12+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.25.11...v0.25.12)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `dashmap` from 6.1.0 to 6.2.1
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v6.1.0...v6.2.1)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `dashmap` from 6.1.0 to 6.2.1
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v6.1.0...v6.2.1)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `dashmap` from 6.1.0 to 6.2.1
- [Release notes](https://github.com/xacrimon/dashmap/releases)
- [Commits](https://github.com/xacrimon/dashmap/compare/v6.1.0...v6.2.1)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `spin` from 0.10.0 to 0.12.0

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `wasm-bindgen-test` from 0.3.71 to 0.3.72
- [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)

Updates `spin` from 0.10.0 to 0.12.0

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: dashmap
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.25.12+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: dashmap
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: dashmap
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: dashmap
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: wasm-bindgen-test
  dependency-version: 0.3.72
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: spin
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

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

* build(deps): refresh Cargo lockfiles

* 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-05-28 14:58:40 -05:00
Anand Krishnamoorthi acf7f7a25e chore: release (#731)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-22 15:50:53 -05:00
Anand Krishnamoorthi 86b4a279fa fix(ffi): eliminate aliasing UB + add Azure Policy JSON compilation FFI (#727)
* fix(ffi): eliminate aliasing UB via to_shared_ref migration

Add to_shared_ref() helper that creates &T (shared reference) from raw
pointers instead of &mut T. This eliminates undefined behavior caused by
violating Rust's aliasing invariant when C# SafeHandle permits concurrent
FFI calls on the same handle.

With &mut T, the compiler may assume exclusive (noalias) access and
reorder or elide reads/writes — a miscompilation risk when another thread
holds a reference to the same object. Switching to &T removes that
assumption; actual mutation is mediated by the interior RwLock inside
Handle<T>, which is the sole synchronization mechanism.

Migrated sites:
- rvm.rs: 20 non-drop call sites
- engine.rs: 30 non-drop call sites + with_unwind_guard for timer fns
- compiled_policy.rs: 2 call sites
- Fix null-data UB in regorus_program_deserialize_binary

Drop paths retain to_ref() where exclusive access is guaranteed by the
caller contract (preventing use-after-free).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(ffi): add Azure Policy JSON compilation FFI and C# bindings

- AliasRegistry builder pattern: RegorusAliasRegistryBuilder (mutable,
  single-threaded) + RegorusAliasRegistry (immutable, Arc-wrapped)
- Azure Policy JSON compilation: regorus_compile_azure_policy_rule and
  regorus_compile_azure_policy_definition with alias registry support
- regorus_rvm_set_context for host-supplied ambient data
- C# AliasRegistryBuilder and AliasRegistry classes with convenience
  factories (FromJson, FromManifest, Empty)
- C# AzurePolicyCompiler static class for policy rule/definition compilation
- Compile functions take *const RegorusAliasRegistry (read-only via
  to_shared_ref for concurrent compilation safety)
- Fix pre-existing clippy warnings across multiple crates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-22 12:50:16 -05:00
81 changed files with 3991 additions and 930 deletions
+15
View File
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.10.1](https://github.com/microsoft/regorus/compare/regorus-v0.10.0...regorus-v0.10.1) - 2026-05-22
### Fixed
- *(ffi)* eliminate aliasing UB + add Azure Policy JSON compilation FFI ([#727](https://github.com/microsoft/regorus/pull/727))
- *(interpreter,rvm)* correct partial object rule iteration and classification ([#718](https://github.com/microsoft/regorus/pull/718))
- *(copilot)* robust diff computation for cloud agent environments ([#709](https://github.com/microsoft/regorus/pull/709))
### Other
- *(azure_policy)* reduce AliasRegistry allocations via Rc sharing ([#725](https://github.com/microsoft/regorus/pull/725))
- *(normalizer)* use Rc<str> interning to reduce alias resolution allocations ([#726](https://github.com/microsoft/regorus/pull/726))
- *(deps)* bump the rust-dependencies group across 5 directories with 2 updates ([#724](https://github.com/microsoft/regorus/pull/724))
- *(deps)* bump the rust-dependencies group across 5 directories with 4 updates ([#717](https://github.com/microsoft/regorus/pull/717))
## [0.10.0] - 2026-05-05 ## [0.10.0] - 2026-05-05
### Added ### Added
Generated
+41 -41
View File
@@ -119,9 +119,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -162,9 +162,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -402,9 +402,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@@ -422,9 +422,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2 1.0.106", "proc-macro2 1.0.106",
"quote 1.0.45", "quote 1.0.45",
@@ -433,9 +433,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "email_address" name = "email_address"
@@ -869,9 +869,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -944,9 +944,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -956,9 +956,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1399,7 +1399,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if", "cfg-if",
@@ -1431,7 +1431,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"spin 0.10.0", "spin 0.12.0",
"test-generator", "test-generator",
"thiserror", "thiserror",
"url", "url",
@@ -1441,7 +1441,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.6" version = "2.2.7"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1518,9 +1518,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1580,9 +1580,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1693,9 +1693,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.11+spec-1.1.0" version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@@ -1841,9 +1841,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1854,9 +1854,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote 1.0.45", "quote 1.0.45",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1864,9 +1864,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2 1.0.106", "proc-macro2 1.0.106",
@@ -1877,9 +1877,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1920,9 +1920,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -2029,9 +2029,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2173,18 +2173,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2 1.0.106", "proc-macro2 1.0.106",
"quote 1.0.45", "quote 1.0.45",
+4 -4
View File
@@ -8,7 +8,7 @@ members = [
[package] [package]
name = "regorus" name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter" description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
license = "MIT AND Apache-2.0 AND BSD-3-Clause" license = "MIT AND Apache-2.0 AND BSD-3-Clause"
repository = "https://github.com/microsoft/regorus" repository = "https://github.com/microsoft/regorus"
@@ -98,7 +98,7 @@ rand = ["dep:rand"]
[dependencies] [dependencies]
anyhow = { version = "1.0.102", default-features = false } anyhow = { version = "1.0.102", default-features = false }
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] } serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] } serde_json = { version = "1.0.150", default-features = false, features = ["alloc"] }
hashbrown = { version = "0.17", default-features = false, features = ["default-hasher"], optional = true } hashbrown = { version = "0.17", default-features = false, features = ["default-hasher"], optional = true }
lazy_static = { version = "1.4.0", default-features = false } lazy_static = { version = "1.4.0", default-features = false }
thiserror = { version = "2.0", default-features = false } thiserror = { version = "2.0", default-features = false }
@@ -107,7 +107,7 @@ data-encoding = { version = "2.8.0", optional = true, default-features=false, fe
num-bigint = { version = "0.4", default-features = false } num-bigint = { version = "0.4", default-features = false }
num-traits = { version = "0.2", default-features = false } num-traits = { version = "0.2", default-features = false }
parking_lot = { version = "0.12", optional = true } parking_lot = { version = "0.12", optional = true }
spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] } spin = { version = "0.12.0", default-features = false, features = ["mutex", "spin_mutex"] }
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true } globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true }
regex = {version = "1.12.3", optional = true, default-features = false } regex = {version = "1.12.3", optional = true, default-features = false }
@@ -128,7 +128,7 @@ rand = { version = "0.10.0", default-features = false, features = ["thread_rng"]
msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true } msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true }
dashmap = { version = "6.1", default-features = false, optional = true } dashmap = { version = "6.1", default-features = false, optional = true }
lru = { version = "0.18", default-features = false, optional = true } lru = { version = "0.18", default-features = false, optional = true }
mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.6", optional = true } mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.7", optional = true }
# rvm related deps # rvm related deps
indexmap = { version = "2.13.1", default-features = false, features = ["serde"], optional = true } indexmap = { version = "2.13.1", default-features = false, features = ["serde"], optional = true }
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RegorusPackageVersion>0.10.0</RegorusPackageVersion> <RegorusPackageVersion>0.10.1</RegorusPackageVersion>
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix> <RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup> </PropertyGroup>
+73
View File
@@ -150,3 +150,76 @@ const string ContextJson = """
var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson); var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson);
Console.WriteLine($"RBAC condition allowed: {allowed}"); Console.WriteLine($"RBAC condition allowed: {allowed}");
``` ```
## Azure Policy JSON Evaluation
Compile and evaluate Azure Policy JSON `policyRule` definitions directly — no Rego translation required.
The `AzurePolicyCompiler` compiles JSON policy rules into RVM programs that can be executed with the `Rvm` engine.
```csharp
using Regorus;
// 1. Load alias definitions for the resource provider
const string AliasesJson = """
[{
"namespace": "Microsoft.Storage",
"resourceTypes": [{
"resourceType": "storageAccounts",
"aliases": [{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
}]
}]
}]
""";
using var registry = AliasRegistry.FromJson(AliasesJson);
// 2. Compile a JSON policy rule (the native Azure Policy language)
const string PolicyRule = """
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false }
]
},
"then": { "effect": "deny" }
}
""";
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, PolicyRule);
// 3. Normalize an ARM resource and evaluate
var armResource = """
{
"type": "Microsoft.Storage/storageAccounts",
"name": "mystorage",
"properties": { "supportsHttpsTrafficOnly": false }
}
""";
var envelope = registry.NormalizeAndWrap(armResource);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(envelope!);
var result = vm.ExecuteEntryPoint("main");
// result: {"effect": "deny"} for non-compliant, "<undefined>" for compliant
Console.WriteLine($"Policy result: {result}");
```
**Context-dependent policies:** If your policy uses context functions like
`subscription()`, `resourceGroup()`, or `requestContext()`, you must also set
the VM context separately:
```csharp
// The context JSON from NormalizeAndWrap is in the input envelope,
// but must also be provided to the VM's ambient context:
vm.SetContextJson(contextJson);
```
You can also compile full policy definitions (with parameters) using
`AzurePolicyCompiler.CompilePolicyDefinition()`. See
`bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs` for comprehensive examples.
@@ -43,31 +43,28 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Create_and_dispose_succeeds() public void Create_and_dispose_succeeds()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.Empty();
Assert.AreEqual(0, registry.Length); Assert.AreEqual(0, registry.Length);
} }
[TestMethod] [TestMethod]
public void LoadJson_populates_registry() public void LoadJson_populates_registry()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(AliasesJson);
registry.LoadJson(AliasesJson);
Assert.AreEqual(1, registry.Length); Assert.AreEqual(1, registry.Length);
} }
[TestMethod] [TestMethod]
public void LoadManifest_populates_registry() public void LoadManifest_populates_registry()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromManifest(ManifestJson);
registry.LoadManifest(ManifestJson);
Assert.AreEqual(1, registry.Length); Assert.AreEqual(1, registry.Length);
} }
[TestMethod] [TestMethod]
public void NormalizeAndWrap_produces_envelope() public void NormalizeAndWrap_produces_envelope()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(AliasesJson);
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -93,8 +90,7 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void NormalizeAndWrap_with_context_and_parameters() public void NormalizeAndWrap_with_context_and_parameters()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(AliasesJson);
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -115,8 +111,7 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Denormalize_restores_properties() public void Denormalize_restores_properties()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(AliasesJson);
registry.LoadJson(AliasesJson);
var normalized = @"{ var normalized = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -137,8 +132,7 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Round_trip_normalize_then_denormalize() public void Round_trip_normalize_then_denormalize()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(AliasesJson);
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -166,8 +160,7 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void DataPlane_manifest_normalize() public void DataPlane_manifest_normalize()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromManifest(ManifestJson);
registry.LoadManifest(ManifestJson);
var resource = @"{ var resource = @"{
""type"": ""Microsoft.KeyVault.Data/vaults/certificates"", ""type"": ""Microsoft.KeyVault.Data/vaults/certificates"",
@@ -185,7 +178,7 @@ public class AliasRegistryTests
[ExpectedException(typeof(InvalidOperationException))] [ExpectedException(typeof(InvalidOperationException))]
public void LoadJson_invalid_throws() public void LoadJson_invalid_throws()
{ {
using var registry = new AliasRegistry(); using var builder = new AliasRegistryBuilder();
registry.LoadJson("not valid json"); builder.LoadJson("not valid json");
} }
} }
@@ -0,0 +1,436 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Text.Json.Nodes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Regorus;
namespace Regorus.Tests;
/// <summary>
/// Tests for <see cref="AzurePolicyCompiler"/> — compiling Azure Policy JSON
/// policyRule and policyDefinition into RVM programs and evaluating them.
/// </summary>
[TestClass]
public class AzurePolicyCompilerTests
{
// -----------------------------------------------------------------------
// Test data
// -----------------------------------------------------------------------
private const string StorageAliasesJson = @"[{
""namespace"": ""Microsoft.Storage"",
""resourceTypes"": [{
""resourceType"": ""storageAccounts"",
""capabilities"": ""SupportsTags, SupportsLocation"",
""aliases"": [
{
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
""paths"": []
},
{
""name"": ""Microsoft.Storage/storageAccounts/minimumTlsVersion"",
""defaultPath"": ""properties.minimumTlsVersion"",
""paths"": []
}
]
}]
}]";
/// <summary>Simple policy rule that checks the resource type.</summary>
private const string SimpleAuditRule = @"{
""if"": {
""field"": ""type"",
""equals"": ""Microsoft.Storage/storageAccounts""
},
""then"": { ""effect"": ""audit"" }
}";
/// <summary>Policy rule that uses an alias to check HTTPS-only.</summary>
private const string HttpsDenyRule = @"{
""if"": {
""allOf"": [
{ ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" },
{ ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false }
]
},
""then"": { ""effect"": ""deny"" }
}";
/// <summary>Full policy definition with parameters.</summary>
private const string PolicyDefinitionWithParams = @"{
""displayName"": ""Require HTTPS for storage accounts"",
""policyType"": ""Custom"",
""mode"": ""Indexed"",
""parameters"": {
""effect"": {
""type"": ""String"",
""defaultValue"": ""deny""
}
},
""policyRule"": {
""if"": {
""allOf"": [
{ ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" },
{ ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false }
]
},
""then"": { ""effect"": ""[parameters('effect')]"" }
}
}";
// -----------------------------------------------------------------------
// Helper
// -----------------------------------------------------------------------
/// <summary>
/// Wrap a normalized resource JSON and parameters into the input envelope
/// expected by compiled Azure Policy RVM programs.
/// </summary>
private static string WrapInput(string resourceJson, string parametersJson = "{}")
{
return $@"{{""resource"": {resourceJson}, ""parameters"": {parametersJson}}}";
}
/// <summary>
/// Compile a policy rule, load it into an RVM, set input, and execute.
/// Returns the result string from <c>ExecuteEntryPoint("main")</c>.
/// </summary>
private static string? CompileAndEval(
AliasRegistry? registry,
string policyRuleJson,
string inputJson)
{
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, policyRuleJson);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(inputJson);
return vm.ExecuteEntryPoint("main");
}
// -----------------------------------------------------------------------
// CompilePolicyRule tests
// -----------------------------------------------------------------------
[TestMethod]
public void CompilePolicyRule_no_aliases_succeeds()
{
using var program = AzurePolicyCompiler.CompilePolicyRule(null, SimpleAuditRule);
Assert.IsNotNull(program);
}
[TestMethod]
public void CompilePolicyRule_with_aliases_succeeds()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule);
Assert.IsNotNull(program);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void CompilePolicyRule_null_json_throws()
{
AzurePolicyCompiler.CompilePolicyRule(null, null!);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void CompilePolicyRule_invalid_json_throws()
{
AzurePolicyCompiler.CompilePolicyRule(null, "not valid json");
}
// -----------------------------------------------------------------------
// CompilePolicyDefinition tests
// -----------------------------------------------------------------------
[TestMethod]
public void CompilePolicyDefinition_no_aliases_succeeds()
{
using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, PolicyDefinitionWithParams);
Assert.IsNotNull(program);
}
[TestMethod]
public void CompilePolicyDefinition_with_aliases_succeeds()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, PolicyDefinitionWithParams);
Assert.IsNotNull(program);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void CompilePolicyDefinition_null_json_throws()
{
AzurePolicyCompiler.CompilePolicyDefinition(null, null!);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void CompilePolicyDefinition_invalid_json_throws()
{
AzurePolicyCompiler.CompilePolicyDefinition(null, @"{""not"": ""a definition""}");
}
// -----------------------------------------------------------------------
// End-to-end evaluation tests
// -----------------------------------------------------------------------
[TestMethod]
public void Eval_simple_rule_matching_resource_returns_effect()
{
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts""}");
var result = CompileAndEval(null, SimpleAuditRule, input);
Assert.IsNotNull(result, "expected a result for matching resource");
var doc = JsonNode.Parse(result!)!;
Assert.AreEqual("audit", doc["effect"]?.GetValue<string>(),
$"expected 'audit' effect, got: {result}");
}
[TestMethod]
public void Eval_simple_rule_non_matching_resource_returns_undefined()
{
var input = WrapInput(
@"{""type"": ""microsoft.compute/virtualmachines""}");
var result = CompileAndEval(null, SimpleAuditRule, input);
Assert.IsNotNull(result);
StringAssert.Contains(result!, "undefined",
"expected undefined for non-matching resource type");
}
[TestMethod]
public void Eval_alias_rule_non_compliant_returns_deny()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
// Non-compliant: HTTPS not enabled (normalized/lowercased form)
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": false}");
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
var doc = JsonNode.Parse(result!)!;
Assert.AreEqual("deny", doc["effect"]?.GetValue<string>(),
$"expected 'deny' for non-compliant resource, got: {result}");
}
[TestMethod]
public void Eval_alias_rule_compliant_returns_undefined()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
// Compliant: HTTPS enabled
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": true}");
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
StringAssert.Contains(result!, "undefined",
"expected undefined for compliant resource");
}
[TestMethod]
public void Eval_definition_with_default_parameters()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
using var program = AzurePolicyCompiler.CompilePolicyDefinition(
registry, PolicyDefinitionWithParams);
using var vm = new Rvm();
vm.LoadProgram(program);
// Non-compliant resource
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": false}");
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
var doc = JsonNode.Parse(result!)!;
// Default parameter value is "deny"
Assert.AreEqual("deny", doc["effect"]?.GetValue<string>(),
$"expected default 'deny' effect, got: {result}");
}
[TestMethod]
public void Eval_with_normalized_arm_resource_end_to_end()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
// Simulate the full production flow:
// 1. Start with an ARM resource
var armResource = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""mystorage"",
""location"": ""eastus"",
""properties"": {
""supportsHttpsTrafficOnly"": false,
""minimumTlsVersion"": ""TLS1_0""
}
}";
// 2. Normalize via AliasRegistry
var normalizedEnvelope = registry.NormalizeAndWrap(
armResource,
apiVersion: null,
contextJson: "{}",
parametersJson: "{}");
Assert.IsNotNull(normalizedEnvelope);
// 3. Compile the policy rule
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule);
// 4. Execute
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(normalizedEnvelope!);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
var doc = JsonNode.Parse(result!)!;
Assert.AreEqual("deny", doc["effect"]?.GetValue<string>(),
$"expected 'deny' for non-HTTPS storage account, got: {result}");
}
[TestMethod]
public void Eval_normalized_compliant_resource_end_to_end()
{
using var registry = AliasRegistry.FromJson(StorageAliasesJson);
var armResource = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""secureastorage"",
""location"": ""westus"",
""properties"": {
""supportsHttpsTrafficOnly"": true,
""minimumTlsVersion"": ""TLS1_2""
}
}";
var normalizedEnvelope = registry.NormalizeAndWrap(
armResource,
apiVersion: null,
contextJson: "{}",
parametersJson: "{}");
Assert.IsNotNull(normalizedEnvelope);
using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson(normalizedEnvelope!);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
StringAssert.Contains(result!, "undefined",
"expected undefined for compliant HTTPS storage account");
}
[TestMethod]
public void Program_can_be_serialized_and_reloaded()
{
using var program = AzurePolicyCompiler.CompilePolicyRule(null, SimpleAuditRule);
// Serialize to binary
var binary = program.SerializeBinary();
Assert.IsTrue(binary.Length > 0, "serialized program should not be empty");
// Deserialize and run
using var restored = Program.DeserializeBinary(binary, out var isPartial);
Assert.IsFalse(isPartial, "program should not be partial");
using var vm = new Rvm();
vm.LoadProgram(restored);
var input = WrapInput(@"{""type"": ""microsoft.storage/storageaccounts""}");
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
var doc = JsonNode.Parse(result!)!;
Assert.AreEqual("audit", doc["effect"]?.GetValue<string>());
}
[TestMethod]
public void Program_generates_listing()
{
using var program = AzurePolicyCompiler.CompilePolicyRule(null, SimpleAuditRule);
var listing = program.GenerateListing();
Assert.IsFalse(string.IsNullOrWhiteSpace(listing),
"generated listing should not be empty");
}
// -----------------------------------------------------------------------
// Context-dependent policy tests
// -----------------------------------------------------------------------
/// Policy rule that uses subscription() context function.
private const string ContextPolicyRule = @"{
""if"": {
""allOf"": [
{ ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" },
{ ""value"": ""[subscription().subscriptionId]"", ""equals"": ""sub-123"" }
]
},
""then"": { ""effect"": ""deny"" }
}";
[TestMethod]
public void Eval_context_policy_with_set_context_returns_effect()
{
using var program = AzurePolicyCompiler.CompilePolicyRule(null, ContextPolicyRule);
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetContextJson(@"{""subscription"": {""subscriptionId"": ""sub-123""}}");
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts""}");
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
var doc = JsonNode.Parse(result!)!;
Assert.AreEqual("deny", doc["effect"]?.GetValue<string>(),
$"expected 'deny' with matching context, got: {result}");
}
[TestMethod]
public void Eval_context_policy_without_context_returns_undefined()
{
using var program = AzurePolicyCompiler.CompilePolicyRule(null, ContextPolicyRule);
using var vm = new Rvm();
vm.LoadProgram(program);
// No context set — subscription() will be undefined
var input = WrapInput(
@"{""type"": ""microsoft.storage/storageaccounts""}");
vm.SetInputJson(input);
var result = vm.ExecuteEntryPoint("main");
Assert.IsNotNull(result);
StringAssert.Contains(result!, "undefined",
"expected undefined without context set");
}
}
@@ -62,8 +62,7 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_produces_input_envelope() public void AliasRegistry_NormalizeAndWrap_produces_input_envelope()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(StorageAliasesJson);
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap( var result = registry.NormalizeAndWrap(
StorageResourceJson, StorageResourceJson,
@@ -84,8 +83,7 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_flattens_properties() public void AliasRegistry_NormalizeAndWrap_flattens_properties()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(StorageAliasesJson);
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap(StorageResourceJson); var result = registry.NormalizeAndWrap(StorageResourceJson);
Assert.IsNotNull(result); Assert.IsNotNull(result);
@@ -107,8 +105,7 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_preserves_type_field() public void AliasRegistry_NormalizeAndWrap_preserves_type_field()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(StorageAliasesJson);
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap(StorageResourceJson); var result = registry.NormalizeAndWrap(StorageResourceJson);
var doc = JsonNode.Parse(result!); var doc = JsonNode.Parse(result!);
@@ -125,8 +122,7 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_includes_parameters() public void AliasRegistry_NormalizeAndWrap_includes_parameters()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(StorageAliasesJson);
registry.LoadJson(StorageAliasesJson);
var parametersJson = @"{ ""effect"": ""Deny"" }"; var parametersJson = @"{ ""effect"": ""Deny"" }";
var result = registry.NormalizeAndWrap( var result = registry.NormalizeAndWrap(
@@ -143,8 +139,7 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_Denormalize_roundtrips_correctly() public void AliasRegistry_Denormalize_roundtrips_correctly()
{ {
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(StorageAliasesJson);
registry.LoadJson(StorageAliasesJson);
// Normalize the ARM resource. // Normalize the ARM resource.
var envelope = registry.NormalizeAndWrap(StorageResourceJson); var envelope = registry.NormalizeAndWrap(StorageResourceJson);
@@ -177,8 +172,7 @@ public class AzurePolicyTests
} }
var aliasesJson = File.ReadAllText(aliasesPath); var aliasesJson = File.ReadAllText(aliasesPath);
using var registry = new AliasRegistry(); using var registry = AliasRegistry.FromJson(aliasesJson);
registry.LoadJson(aliasesJson);
// The test_aliases.json file contains multiple providers. // The test_aliases.json file contains multiple providers.
Assert.IsTrue(registry.Length > 0, Assert.IsTrue(registry.Length > 0,
+41 -66
View File
@@ -8,51 +8,43 @@ using Regorus.Internal;
namespace Regorus namespace Regorus
{ {
/// <summary> /// <summary>
/// Manages Azure Policy alias definitions used for resource normalization /// Immutable Azure Policy alias registry used for resource normalization
/// and policy compilation. /// and policy compilation.
/// </summary> /// </summary>
public unsafe sealed class AliasRegistry : SafeHandleWrapper public unsafe sealed class AliasRegistry : SafeHandleWrapper
{ {
/// <summary> internal AliasRegistry(RegorusAliasRegistryHandle handle)
/// Create an empty alias registry. : base(handle, nameof(AliasRegistry))
/// </summary>
public AliasRegistry()
: base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry))
{ {
} }
/// <summary> /// <summary>
/// Load control-plane alias data (array of ProviderAliases) from a JSON string. /// Create an empty immutable alias registry.
/// </summary> /// </summary>
/// <param name="json">JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json)</param> public static AliasRegistry Empty()
public void LoadJson(string json)
{ {
Utf8Marshaller.WithUtf8(json, jsonPtr => using var builder = new AliasRegistryBuilder();
{ return builder.Build();
UseHandle(regPtr =>
{
CheckAndDropResult(API.regorus_alias_registry_load_json(
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
return 0;
});
});
} }
/// <summary> /// <summary>
/// Load a data-plane policy manifest from a JSON string. /// Create an immutable alias registry from control-plane alias JSON.
/// </summary> /// </summary>
/// <param name="json">JSON object containing a DataPolicyManifest</param> public static AliasRegistry FromJson(string json)
public void LoadManifest(string json)
{ {
Utf8Marshaller.WithUtf8(json, jsonPtr => using var builder = new AliasRegistryBuilder();
{ builder.LoadJson(json);
UseHandle(regPtr => return builder.Build();
{ }
CheckAndDropResult(API.regorus_alias_registry_load_manifest(
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr)); /// <summary>
return 0; /// Create an immutable alias registry from a data-plane manifest JSON document.
}); /// </summary>
}); public static AliasRegistry FromManifest(string json)
{
using var builder = new AliasRegistryBuilder();
builder.LoadManifest(json);
return builder.Build();
} }
/// <summary> /// <summary>
@@ -74,11 +66,6 @@ namespace Regorus
/// Normalize an ARM resource JSON and wrap it into the standard input envelope /// Normalize an ARM resource JSON and wrap it into the standard input envelope
/// expected by a compiled Azure Policy program. /// expected by a compiled Azure Policy program.
/// </summary> /// </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 = "{}") public string? NormalizeAndWrap(string resourceJson, string? apiVersion = null, string contextJson = "{}", string parametersJson = "{}")
{ {
return Utf8Marshaller.WithUtf8(resourceJson, resPtr => return Utf8Marshaller.WithUtf8(resourceJson, resPtr =>
@@ -96,27 +83,22 @@ namespace Regorus
(byte*)ctxPtr, (byte*)paramsPtr)); (byte*)ctxPtr, (byte*)paramsPtr));
}); });
} }
else
{ return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => UseHandle(regPtr =>
UseHandle(regPtr => {
{ return ResultHelpers.GetStringResult(
return ResultHelpers.GetStringResult( API.regorus_alias_registry_normalize_and_wrap(
API.regorus_alias_registry_normalize_and_wrap( (RegorusAliasRegistry*)regPtr,
(RegorusAliasRegistry*)regPtr, (byte*)resPtr, (byte*)apiPtr,
(byte*)resPtr, (byte*)apiPtr, (byte*)ctxPtr, (byte*)paramsPtr));
(byte*)ctxPtr, (byte*)paramsPtr)); }));
}));
}
}))); })));
} }
/// <summary> /// <summary>
/// Denormalize a previously-normalized resource JSON back to ARM format. /// Denormalize a previously-normalized resource JSON back to ARM format.
/// </summary> /// </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) public string? Denormalize(string normalizedJson, string? apiVersion = null)
{ {
return Utf8Marshaller.WithUtf8(normalizedJson, normPtr => return Utf8Marshaller.WithUtf8(normalizedJson, normPtr =>
@@ -131,23 +113,16 @@ namespace Regorus
(byte*)normPtr, null)); (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 Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
{ UseHandle(regPtr =>
return ResultHelpers.GetStringResult(result); {
return ResultHelpers.GetStringResult(
API.regorus_alias_registry_denormalize(
(RegorusAliasRegistry*)regPtr,
(byte*)normPtr, (byte*)apiPtr));
}));
});
} }
} }
} }
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Mutable, single-threaded builder for <see cref="AliasRegistry"/>.
/// Load alias data, then call <see cref="Build"/> to freeze the registry.
/// </summary>
public unsafe sealed class AliasRegistryBuilder : SafeHandleWrapper
{
/// <summary>
/// Create an empty alias registry builder.
/// </summary>
public AliasRegistryBuilder()
: base(RegorusAliasRegistryBuilderHandle.Create(), nameof(AliasRegistryBuilder))
{
}
/// <summary>
/// Load control-plane alias data (array of ProviderAliases) from a JSON string.
/// </summary>
public void LoadJson(string json)
{
Utf8Marshaller.WithUtf8(json, jsonPtr =>
{
UseHandle(builderPtr =>
{
ResultHelpers.GetStringResult(API.regorus_alias_registry_builder_load_json(
(RegorusAliasRegistryBuilder*)builderPtr,
(byte*)jsonPtr));
});
});
}
/// <summary>
/// Load a data-plane policy manifest from a JSON string.
/// </summary>
public void LoadManifest(string json)
{
Utf8Marshaller.WithUtf8(json, jsonPtr =>
{
UseHandle(builderPtr =>
{
ResultHelpers.GetStringResult(API.regorus_alias_registry_builder_load_manifest(
(RegorusAliasRegistryBuilder*)builderPtr,
(byte*)jsonPtr));
});
});
}
/// <summary>
/// Freeze the builder into an immutable, thread-safe alias registry.
/// </summary>
public AliasRegistry Build()
{
return UseHandle(builderPtr =>
{
var registryPtr = ResultHelpers.GetPointerResult(
API.regorus_alias_registry_builder_build((RegorusAliasRegistryBuilder*)builderPtr));
return new AliasRegistry(RegorusAliasRegistryHandle.FromPointer(registryPtr));
});
}
}
}
@@ -0,0 +1,183 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using Regorus.Internal;
#nullable enable
namespace Regorus
{
/// <summary>
/// Provides static methods for compiling Azure Policy JSON definitions
/// into RVM programs that can be executed by <see cref="Rvm"/>.
/// </summary>
/// <remarks>
/// <para>
/// This class bridges the gap between Azure Policy JSON (the native
/// Azure policy language with <c>policyRule</c>, <c>field</c>,
/// <c>equals</c>, etc.) and Regorus's RVM execution engine.
/// </para>
///
/// <para>
/// <b>Typical workflow:</b>
/// </para>
/// <list type="number">
/// <item>Load alias definitions with <see cref="AliasRegistryBuilder"/> and freeze them into an <see cref="AliasRegistry"/>.</item>
/// <item>Normalize the ARM resource via <see cref="AliasRegistry.NormalizeAndWrap"/>.</item>
/// <item>Compile the JSON policyRule with <see cref="CompilePolicyRule"/> or the
/// full definition with <see cref="CompilePolicyDefinition"/>.</item>
/// <item>Execute the resulting <see cref="Program"/> in an <see cref="Rvm"/>
/// instance with the normalized input.</item>
/// </list>
///
/// <para>
/// <b>Context-dependent policies:</b> Policies that use context functions
/// such as <c>subscription()</c>, <c>resourceGroup()</c>, or
/// <c>requestContext()</c> require the VM context to be set separately via
/// <see cref="Rvm.SetContextJson"/> before execution. The context JSON
/// returned by <see cref="AliasRegistry.NormalizeAndWrap"/> is passed as
/// <c>input.context</c> but is <b>not</b> automatically wired into the VM's
/// ambient context — the caller must do both:
/// <c>vm.SetInputJson(envelope)</c> and <c>vm.SetContextJson(contextJson)</c>.
/// </para>
/// </remarks>
public static unsafe class AzurePolicyCompiler
{
/// <summary>
/// Compile an Azure Policy JSON policy rule into an RVM <see cref="Program"/>.
/// </summary>
/// <param name="aliasRegistry">
/// Alias registry for resolving fully-qualified alias names in field
/// references. Pass <c>null</c> if no alias resolution is needed.
/// <para>
/// <b>Warning:</b> When <c>null</c>, alias field references compile as raw
/// property paths and will silently produce incorrect evaluation results for
/// policies that use aliases. Modify/Append effect policies will also skip
/// the compile-time modifiability validation. Only pass <c>null</c> when the
/// policy is known to contain no alias references (e.g. simple type/location
/// checks or unit-test scenarios).
/// </para>
/// </param>
/// <param name="policyRuleJson">
/// JSON string containing the policyRule object, e.g.
/// <c>{ "if": { "field": "type", "equals": "..." }, "then": { "effect": "deny" } }</c>
/// </param>
/// <returns>
/// A compiled <see cref="Program"/> ready to be loaded into an
/// <see cref="Rvm"/> instance.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="policyRuleJson"/> is <c>null</c>.
/// </exception>
/// <exception cref="Exception">
/// Thrown when parsing or compilation fails.
/// </exception>
public static Program CompilePolicyRule(AliasRegistry? aliasRegistry, string policyRuleJson)
{
if (policyRuleJson is null)
{
throw new ArgumentNullException(nameof(policyRuleJson));
}
return Utf8Marshaller.WithUtf8(policyRuleJson, rulePtr =>
{
if (aliasRegistry is null)
{
var result = API.regorus_compile_azure_policy_rule(
null, (byte*)rulePtr);
return GetProgramResult(result);
}
else
{
return aliasRegistry.UseHandleForInterop(regPtr =>
{
var result = API.regorus_compile_azure_policy_rule(
(RegorusAliasRegistry*)regPtr, (byte*)rulePtr);
return GetProgramResult(result);
});
}
});
}
/// <summary>
/// Compile a full Azure Policy definition JSON into an RVM <see cref="Program"/>.
/// </summary>
/// <param name="aliasRegistry">
/// Alias registry for resolving fully-qualified alias names in field
/// references. Pass <c>null</c> if no alias resolution is needed.
/// <para>
/// <b>Warning:</b> When <c>null</c>, alias field references compile as raw
/// property paths and will silently produce incorrect evaluation results for
/// policies that use aliases. Modify/Append effect policies will also skip
/// the compile-time modifiability validation. Only pass <c>null</c> when the
/// policy is known to contain no alias references (e.g. simple type/location
/// checks or unit-test scenarios).
/// </para>
/// </param>
/// <param name="policyDefinitionJson">
/// JSON string containing the full policy definition, which includes
/// <c>policyRule</c>, <c>parameters</c>, <c>displayName</c>, etc.
/// Accepted in both wrapped and unwrapped forms.
/// </param>
/// <returns>
/// A compiled <see cref="Program"/> ready to be loaded into an
/// <see cref="Rvm"/> instance.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="policyDefinitionJson"/> is <c>null</c>.
/// </exception>
/// <exception cref="Exception">
/// Thrown when parsing or compilation fails.
/// </exception>
public static Program CompilePolicyDefinition(AliasRegistry? aliasRegistry, string policyDefinitionJson)
{
if (policyDefinitionJson is null)
{
throw new ArgumentNullException(nameof(policyDefinitionJson));
}
return Utf8Marshaller.WithUtf8(policyDefinitionJson, defnPtr =>
{
if (aliasRegistry is null)
{
var result = API.regorus_compile_azure_policy_definition(
null, (byte*)defnPtr);
return GetProgramResult(result);
}
else
{
return aliasRegistry.UseHandleForInterop(regPtr =>
{
var result = API.regorus_compile_azure_policy_definition(
(RegorusAliasRegistry*)regPtr, (byte*)defnPtr);
return GetProgramResult(result);
});
}
});
}
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);
}
}
}
}
+57 -15
View File
@@ -178,6 +178,14 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_input", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [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); internal static extern RegorusResult regorus_rvm_set_input(RegorusRvm* vm, byte* input_json);
/// <summary>
/// Set the context document for the RVM.
/// The context provides host-supplied ambient data (e.g. resourceGroup(), subscription())
/// that Azure Policy functions can access.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rvm_set_context", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rvm_set_context(RegorusRvm* vm, byte* context_json);
/// <summary> /// <summary>
/// Execute the program. /// Execute the program.
/// </summary> /// </summary>
@@ -490,6 +498,20 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_compile_policy_for_target", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_compile_policy_for_target", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compile_policy_for_target(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len); internal static extern RegorusResult regorus_compile_policy_for_target(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len);
/// <summary>
/// Compile an Azure Policy JSON policy rule into an RVM program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compile_azure_policy_rule", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compile_azure_policy_rule(
RegorusAliasRegistry* registry, byte* policy_rule_json);
/// <summary>
/// Compile a full Azure Policy definition JSON into an RVM program.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_compile_azure_policy_definition", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_compile_azure_policy_definition(
RegorusAliasRegistry* registry, byte* policy_definition_json);
#endregion #endregion
#region Compiled Policy Methods #region Compiled Policy Methods
@@ -673,10 +695,34 @@ namespace Regorus.Internal
#region Alias Registry Methods #region Alias Registry Methods
/// <summary> /// <summary>
/// Create a new, empty AliasRegistry. /// Create a new alias registry builder.
/// </summary> /// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusAliasRegistry* regorus_alias_registry_new(); internal static extern RegorusAliasRegistryBuilder* regorus_alias_registry_builder_new();
/// <summary>
/// Drop an alias registry builder.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_alias_registry_builder_drop(RegorusAliasRegistryBuilder* builder);
/// <summary>
/// Load control-plane alias data into the builder.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_builder_load_json(RegorusAliasRegistryBuilder* builder, byte* json);
/// <summary>
/// Load a data-plane policy manifest into the builder.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_builder_load_manifest(RegorusAliasRegistryBuilder* builder, byte* json);
/// <summary>
/// Freeze a builder into an immutable alias registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_build", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_builder_build(RegorusAliasRegistryBuilder* builder);
/// <summary> /// <summary>
/// Drop an AliasRegistry. /// Drop an AliasRegistry.
@@ -684,18 +730,6 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry); 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> /// <summary>
/// Return the number of resource types loaded in the alias registry. /// Return the number of resource types loaded in the alias registry.
/// </summary> /// </summary>
@@ -923,6 +957,14 @@ namespace Regorus.Internal
public byte* content; public byte* content;
} }
/// <summary>
/// Wrapper for AliasRegistryBuilder.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusAliasRegistryBuilder
{
}
/// <summary> /// <summary>
/// Wrapper for AliasRegistry. /// Wrapper for AliasRegistry.
/// </summary> /// </summary>
+1 -1
View File
@@ -15,7 +15,7 @@ namespace Regorus
/// </summary> /// </summary>
public unsafe sealed class Program : SafeHandleWrapper public unsafe sealed class Program : SafeHandleWrapper
{ {
private Program(RegorusProgramHandle handle) internal Program(RegorusProgramHandle handle)
: base(handle, nameof(Program)) : base(handle, nameof(Program))
{ {
} }
+24
View File
@@ -69,5 +69,29 @@ namespace Regorus.Internal
API.regorus_result_drop(result); API.regorus_result_drop(result);
} }
} }
internal static IntPtr GetPointerResult(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 InvalidOperationException("Expected pointer result.");
}
return (IntPtr)result.pointer_value;
}
finally
{
API.regorus_result_drop(result);
}
}
} }
} }
+18
View File
@@ -106,6 +106,24 @@ namespace Regorus
}); });
} }
/// <summary>
/// Set the context document for the VM.
/// The context provides host-supplied ambient data (e.g. resourceGroup(),
/// subscription()) that Azure Policy functions can access via LoadContext
/// instructions.
/// </summary>
public void SetContextJson(string contextJson)
{
Utf8Marshaller.WithUtf8(contextJson, contextPtr =>
{
UseHandle(vmPtr =>
{
CheckAndDropResult(API.regorus_rvm_set_context((RegorusRvm*)vmPtr, (byte*)contextPtr));
return 0;
});
});
}
/// <summary> /// <summary>
/// Set the execution mode (0 = run-to-completion, 1 = suspendable). /// Set the execution mode (0 = run-to-completion, 1 = suspendable).
/// </summary> /// </summary>
+36 -16
View File
@@ -184,28 +184,48 @@ namespace Regorus
} }
} }
internal sealed class RegorusAliasRegistryBuilderHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusAliasRegistryBuilderHandle() : base(ownsHandle: true)
{
}
internal static RegorusAliasRegistryBuilderHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_alias_registry_builder_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus alias registry builder.");
}
var handle = new RegorusAliasRegistryBuilderHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
unsafe
{
Internal.API.regorus_alias_registry_builder_drop((Internal.RegorusAliasRegistryBuilder*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid
{ {
private RegorusAliasRegistryHandle() : base(ownsHandle: true) 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) internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer)
{ {
if (pointer == IntPtr.Zero) if (pointer == IntPtr.Zero)
@@ -232,6 +232,9 @@ allow if {
Console.WriteLine("\n8. RVM host await (suspend/resume):"); Console.WriteLine("\n8. RVM host await (suspend/resume):");
DemonstrateRvmHostAwait(); DemonstrateRvmHostAwait();
Console.WriteLine("\n9. Azure Policy JSON compilation:");
DemonstrateAzurePolicyJsonCompilation();
} }
static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy) static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy)
@@ -492,4 +495,80 @@ allow if {
var resumed = vm.Resume("{\"tier\":\"gold\"}"); var resumed = vm.Resume("{\"tier\":\"gold\"}");
Console.WriteLine($"HostAwait resumed result: {resumed}"); Console.WriteLine($"HostAwait resumed result: {resumed}");
} }
// Azure Policy JSON constants
private const string STORAGE_ALIASES_JSON = @"[{
""namespace"": ""Microsoft.Storage"",
""resourceTypes"": [{
""resourceType"": ""storageAccounts"",
""capabilities"": ""SupportsTags, SupportsLocation"",
""aliases"": [
{
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
""paths"": []
}
]
}]
}]";
private const string HTTPS_DENY_RULE = @"{
""if"": {
""allOf"": [
{ ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" },
{ ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false }
]
},
""then"": { ""effect"": ""deny"" }
}";
static void DemonstrateAzurePolicyJsonCompilation()
{
// 1. Set up alias registry
using var registry = Regorus.AliasRegistry.FromJson(STORAGE_ALIASES_JSON);
Console.WriteLine("Loaded storage account aliases");
// 2. Compile the JSON policy rule directly (no Rego needed)
using var program = Regorus.AzurePolicyCompiler.CompilePolicyRule(registry, HTTPS_DENY_RULE);
Console.WriteLine("Compiled Azure Policy JSON rule to RVM program");
// 3. Normalize an ARM resource
var armResource = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""insecurestorage"",
""location"": ""eastus"",
""properties"": { ""supportsHttpsTrafficOnly"": false }
}";
var envelope = registry.NormalizeAndWrap(armResource, apiVersion: null, contextJson: "{}", parametersJson: "{}");
Console.WriteLine($"Normalized ARM resource to evaluation envelope");
// 4. Execute in the RVM
// Note: For policies using context functions (subscription(), resourceGroup()),
// call vm.SetContextJson(contextJson) before execution. The context from
// NormalizeAndWrap is in the envelope but must also be set on the VM separately.
using var vm = new Regorus.Rvm();
vm.LoadProgram(program);
vm.SetInputJson(envelope!);
// vm.SetContextJson(contextJson); // ← required for context-dependent policies
var result = vm.ExecuteEntryPoint("main");
Console.WriteLine($"Evaluation result (non-compliant): {result}");
// 5. Test with a compliant resource
var compliantResource = @"{
""type"": ""Microsoft.Storage/storageAccounts"",
""name"": ""securestorage"",
""location"": ""eastus"",
""properties"": { ""supportsHttpsTrafficOnly"": true }
}";
var compliantEnvelope = registry.NormalizeAndWrap(compliantResource, apiVersion: null, contextJson: "{}", parametersJson: "{}");
using var vm2 = new Regorus.Rvm();
vm2.LoadProgram(program);
vm2.SetInputJson(compliantEnvelope!);
var compliantResult = vm2.ExecuteEntryPoint("main");
Console.WriteLine($"Evaluation result (compliant): {compliantResult}");
// 6. Demonstrate program serialization
var binary = program.SerializeBinary();
Console.WriteLine($"Serialized program size: {binary.Length} bytes");
}
} }
+38 -38
View File
@@ -98,9 +98,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -141,9 +141,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -153,9 +153,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "cbindgen" name = "cbindgen"
version = "0.29.2" version = "0.29.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" checksum = "c95537b45400390270fae69ac098d057c8f5399001cde9d04f700c105ddfff2d"
dependencies = [ dependencies = [
"clap", "clap",
"heck", "heck",
@@ -285,9 +285,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@@ -305,9 +305,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -712,9 +712,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -790,9 +790,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -802,9 +802,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1128,7 +1128,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1163,7 +1163,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-ffi" name = "regorus-ffi"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cbindgen", "cbindgen",
@@ -1174,7 +1174,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.6" version = "2.2.7"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1255,9 +1255,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1314,9 +1314,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1426,7 +1426,7 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow 1.0.2", "winnow 1.0.3",
] ]
[[package]] [[package]]
@@ -1535,9 +1535,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1548,9 +1548,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1558,9 +1558,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1571,9 +1571,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1688,9 +1688,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
@@ -1817,18 +1817,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+2 -2
View File
@@ -2,7 +2,7 @@
[package] [package]
name = "regorus-ffi" name = "regorus-ffi"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
license = "MIT AND Apache-2.0 AND BSD-3-Clause" license = "MIT AND Apache-2.0 AND BSD-3-Clause"
@@ -13,7 +13,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
regorus = { path = "../..", default-features = false } regorus = { path = "../..", default-features = false }
serde_json = "1.0.140" serde_json = "1.0.150"
parking_lot = { version = "0.12", optional = true } parking_lot = { version = "0.12", optional = true }
[profile.release] [profile.release]
+204 -98
View File
@@ -5,66 +5,108 @@
#![cfg(feature = "azure_policy")] #![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus}; use crate::common::{from_c_str, to_ref, to_shared_ref, RegorusResult, RegorusStatus};
use crate::panic_guard::with_unwind_guard; use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::format; use alloc::format;
use alloc::string::String; use alloc::string::String;
use anyhow::Result; use alloc::sync::Arc;
use core::ffi::c_char; use anyhow::{anyhow, Result};
use core::ptr; use core::ffi::{c_char, c_void};
use core::{mem, ptr};
use regorus::languages::azure_policy::aliases::AliasRegistry; use regorus::languages::azure_policy::aliases::AliasRegistry;
/// Opaque wrapper for `AliasRegistry`. /// Mutable builder 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. /// This handle is intentionally single-threaded and must not be used
#[no_mangle] /// concurrently. Callers should finish loading alias data and then freeze it
pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry { /// into a `RegorusAliasRegistry` via `regorus_alias_registry_builder_build`.
let wrapper = RegorusAliasRegistry { pub struct RegorusAliasRegistryBuilder {
registry: AliasRegistry::new(), registry: AliasRegistry,
}; built: bool,
Box::into_raw(Box::new(wrapper))
} }
/// Drop a `RegorusAliasRegistry`. impl RegorusAliasRegistryBuilder {
fn new() -> Self {
Self {
registry: AliasRegistry::new(),
built: false,
}
}
fn registry_mut(&mut self) -> Result<&mut AliasRegistry> {
if self.built {
return Err(anyhow!("alias registry builder has already been built"));
}
Ok(&mut self.registry)
}
fn build(&mut self) -> Result<RegorusAliasRegistry> {
if self.built {
return Err(anyhow!("alias registry builder has already been built"));
}
self.built = true;
Ok(RegorusAliasRegistry {
registry: Arc::new(mem::replace(&mut self.registry, AliasRegistry::new())),
})
}
}
/// Frozen, immutable alias registry.
pub struct RegorusAliasRegistry {
registry: Arc<AliasRegistry>,
}
impl RegorusAliasRegistry {
/// Return a shared reference to the inner registry for use by the compiler.
pub(crate) fn inner(&self) -> Arc<AliasRegistry> {
Arc::clone(&self.registry)
}
}
// ---------------------------------------------------------------------------
// Builder lifecycle
// ---------------------------------------------------------------------------
/// Create a new, empty `AliasRegistry` builder.
///
/// The caller must eventually call `regorus_alias_registry_builder_drop`.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) { pub extern "C" fn regorus_alias_registry_builder_new() -> *mut RegorusAliasRegistryBuilder {
if let Ok(r) = to_ref(registry) { Box::into_raw(Box::new(RegorusAliasRegistryBuilder::new()))
}
/// Drop a `RegorusAliasRegistryBuilder`.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_builder_drop(builder: *mut RegorusAliasRegistryBuilder) {
if let Ok(builder) = to_ref(builder) {
unsafe { unsafe {
let _ = Box::from_raw(ptr::from_mut(r)); let _ = Box::from_raw(ptr::from_mut(builder));
} }
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Loading // Builder loading
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Load control-plane alias data (array of `ProviderAliases`) into the registry. /// Load control-plane alias data (array of `ProviderAliases`) into the builder.
/// ///
/// `json` must be a valid null-terminated UTF-8 string containing the JSON /// `json` must be a valid null-terminated UTF-8 string containing the JSON
/// array returned by `Get-AzPolicyAlias` or the static /// array returned by `Get-AzPolicyAlias` or the static
/// `ResourceTypesAndAliases.json` file. /// `ResourceTypesAndAliases.json` file.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_load_json( pub extern "C" fn regorus_alias_registry_builder_load_json(
registry: *mut RegorusAliasRegistry, builder: *mut RegorusAliasRegistryBuilder,
json: *const c_char, json: *const c_char,
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<()> { let output = || -> Result<()> {
let json_str = from_c_str(json)?; let json_str = from_c_str(json)?;
to_ref(registry)?.registry.load_from_json(&json_str)?; to_ref(builder)?.registry_mut()?.load_from_json(&json_str)?;
Ok(()) Ok(())
}(); }();
@@ -78,20 +120,20 @@ pub extern "C" fn regorus_alias_registry_load_json(
}) })
} }
/// Load a data-plane policy manifest into the registry. /// Load a data-plane policy manifest into the builder.
/// ///
/// `json` must be a valid null-terminated UTF-8 string containing a single /// `json` must be a valid null-terminated UTF-8 string containing a single
/// `DataPolicyManifest` JSON object. /// `DataPolicyManifest` JSON object.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_load_manifest( pub extern "C" fn regorus_alias_registry_builder_load_manifest(
registry: *mut RegorusAliasRegistry, builder: *mut RegorusAliasRegistryBuilder,
json: *const c_char, json: *const c_char,
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<()> { let output = || -> Result<()> {
let json_str = from_c_str(json)?; let json_str = from_c_str(json)?;
to_ref(registry)? to_ref(builder)?
.registry .registry_mut()?
.load_data_policy_manifest_json(&json_str)?; .load_data_policy_manifest_json(&json_str)?;
Ok(()) Ok(())
}(); }();
@@ -106,16 +148,52 @@ pub extern "C" fn regorus_alias_registry_load_manifest(
}) })
} }
/// Freeze a builder into an immutable `RegorusAliasRegistry`.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_builder_build(
builder: *mut RegorusAliasRegistryBuilder,
) -> RegorusResult {
with_unwind_guard(|| {
let output = || -> Result<*mut RegorusAliasRegistry> {
let registry = to_ref(builder)?.build()?;
Ok(Box::into_raw(Box::new(registry)))
}();
match output {
Ok(registry) => RegorusResult::ok_pointer(registry as *mut c_void),
Err(e) => {
RegorusResult::err_with_message(RegorusStatus::InvalidArgument, format!("{e}"))
}
}
})
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Queries // Frozen registry lifecycle
// ---------------------------------------------------------------------------
/// Drop a `RegorusAliasRegistry`.
#[no_mangle]
pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) {
if let Ok(registry) = to_ref(registry) {
unsafe {
let _ = Box::from_raw(ptr::from_mut(registry));
}
}
}
// ---------------------------------------------------------------------------
// Frozen registry queries
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Return the number of resource types loaded in the alias registry. /// Return the number of resource types loaded in the alias registry.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult { pub extern "C" fn regorus_alias_registry_len(
registry: *const RegorusAliasRegistry,
) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<i64> { let output = || -> Result<i64> {
let len = to_ref(registry)?.registry.len(); let len = to_shared_ref(registry)?.registry.len();
Ok(len as i64) Ok(len as i64)
}(); }();
@@ -134,15 +212,9 @@ pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry
/// ///
/// Returns a JSON string: /// Returns a JSON string:
/// `{ "resource": <normalized>, "context": <context>, "parameters": <params> }`. /// `{ "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] #[no_mangle]
pub extern "C" fn regorus_alias_registry_normalize_and_wrap( pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
registry: *mut RegorusAliasRegistry, registry: *const RegorusAliasRegistry,
resource_json: *const c_char, resource_json: *const c_char,
api_version: *const c_char, api_version: *const c_char,
context_json: *const c_char, context_json: *const c_char,
@@ -168,7 +240,7 @@ pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
let context = regorus::Value::from_json_str(&context_str)?; let context = regorus::Value::from_json_str(&context_str)?;
let params = regorus::Value::from_json_str(&params_str)?; let params = regorus::Value::from_json_str(&params_str)?;
let wrapped = to_ref(registry)?.registry.normalize_and_wrap( let wrapped = to_shared_ref(registry)?.registry.normalize_and_wrap(
&resource, &resource,
api_ver.as_deref(), api_ver.as_deref(),
Some(context), Some(context),
@@ -185,14 +257,9 @@ pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
} }
/// Denormalize a previously-normalized resource JSON back to ARM format. /// 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] #[no_mangle]
pub extern "C" fn regorus_alias_registry_denormalize( pub extern "C" fn regorus_alias_registry_denormalize(
registry: *mut RegorusAliasRegistry, registry: *const RegorusAliasRegistry,
normalized_json: *const c_char, normalized_json: *const c_char,
api_version: *const c_char, api_version: *const c_char,
) -> RegorusResult { ) -> RegorusResult {
@@ -212,7 +279,7 @@ pub extern "C" fn regorus_alias_registry_denormalize(
let normalized = regorus::Value::from_json_str(&normalized_str)?; let normalized = regorus::Value::from_json_str(&normalized_str)?;
let result = to_ref(registry)? let result = to_shared_ref(registry)?
.registry .registry
.denormalize(&normalized, api_ver.as_deref()); .denormalize(&normalized, api_ver.as_deref());
result.to_json_str() result.to_json_str()
@@ -232,12 +299,10 @@ mod tests {
use core::ffi::CStr; use core::ffi::CStr;
use std::ffi::CString; use std::ffi::CString;
/// Helper: create a C string from a Rust &str.
fn c(s: &str) -> CString { fn c(s: &str) -> CString {
CString::new(s).expect("CString::new failed") 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 { fn assert_ok_string(r: &RegorusResult) -> String {
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
assert!(!r.output.is_null(), "expected non-null output"); assert!(!r.output.is_null(), "expected non-null output");
@@ -248,12 +313,51 @@ mod tests {
s s
} }
/// Helper: assert a RegorusResult has Ok status with integer output.
fn assert_ok_int(r: &RegorusResult) -> i64 { fn assert_ok_int(r: &RegorusResult) -> i64 {
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
r.int_value r.int_value
} }
fn assert_ok_pointer(r: &RegorusResult) -> *mut c_void {
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
assert!(matches!(
r.data_type,
crate::common::RegorusDataType::Pointer
));
assert!(!r.pointer_value.is_null());
r.pointer_value
}
fn build_registry_with_json(json: &str) -> *mut RegorusAliasRegistry {
let builder = regorus_alias_registry_builder_new();
let json = c(json);
let r = regorus_alias_registry_builder_load_json(builder, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_builder_build(builder);
let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry;
regorus_result_drop(r);
regorus_alias_registry_builder_drop(builder);
registry
}
fn build_registry_with_manifest(json: &str) -> *mut RegorusAliasRegistry {
let builder = regorus_alias_registry_builder_new();
let json = c(json);
let r = regorus_alias_registry_builder_load_manifest(builder, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_builder_build(builder);
let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry;
regorus_result_drop(r);
regorus_alias_registry_builder_drop(builder);
registry
}
const ALIASES: &str = r#"[{ const ALIASES: &str = r#"[{
"namespace": "Microsoft.Storage", "namespace": "Microsoft.Storage",
"resourceTypes": [{ "resourceTypes": [{
@@ -279,20 +383,21 @@ mod tests {
}"#; }"#;
#[test] #[test]
fn lifecycle_new_and_drop() { fn lifecycle_builder_build_and_drop() {
let reg = regorus_alias_registry_new(); let builder = regorus_alias_registry_builder_new();
assert!(!reg.is_null()); assert!(!builder.is_null());
regorus_alias_registry_drop(reg);
let r = regorus_alias_registry_builder_build(builder);
let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry;
regorus_result_drop(r);
regorus_alias_registry_builder_drop(builder);
regorus_alias_registry_drop(registry);
} }
#[test] #[test]
fn load_json_and_check_len() { fn load_json_and_check_len() {
let reg = regorus_alias_registry_new(); let reg = build_registry_with_json(ALIASES);
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); let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1); assert_eq!(assert_ok_int(&r), 1);
@@ -303,12 +408,7 @@ mod tests {
#[test] #[test]
fn load_manifest_and_check_len() { fn load_manifest_and_check_len() {
let reg = regorus_alias_registry_new(); let reg = build_registry_with_manifest(MANIFEST);
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); let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1); assert_eq!(assert_ok_int(&r), 1);
@@ -319,23 +419,39 @@ mod tests {
#[test] #[test]
fn load_invalid_json_returns_error() { fn load_invalid_json_returns_error() {
let reg = regorus_alias_registry_new(); let builder = regorus_alias_registry_builder_new();
let bad = c("not valid json"); let bad = c("not valid json");
let r = regorus_alias_registry_load_json(reg, bad.as_ptr()); let r = regorus_alias_registry_builder_load_json(builder, bad.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok); assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r); regorus_result_drop(r);
regorus_alias_registry_drop(reg); regorus_alias_registry_builder_drop(builder);
}
#[test]
fn builder_cannot_be_reused_after_build() {
let builder = regorus_alias_registry_builder_new();
let r = regorus_alias_registry_builder_build(builder);
let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry;
regorus_result_drop(r);
let aliases = c(ALIASES);
let r = regorus_alias_registry_builder_load_json(builder, aliases.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_builder_build(builder);
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
regorus_alias_registry_builder_drop(builder);
regorus_alias_registry_drop(registry);
} }
#[test] #[test]
fn normalize_and_wrap_round_trip() { fn normalize_and_wrap_round_trip() {
let reg = regorus_alias_registry_new(); let reg = build_registry_with_json(ALIASES);
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#"{ let resource = c(r#"{
"name": "acct1", "name": "acct1",
@@ -346,7 +462,6 @@ mod tests {
let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#); let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#);
let params = c(r#"{"env": "prod"}"#); let params = c(r#"{"env": "prod"}"#);
// Normalize
let r = regorus_alias_registry_normalize_and_wrap( let r = regorus_alias_registry_normalize_and_wrap(
reg, reg,
resource.as_ptr(), resource.as_ptr(),
@@ -357,7 +472,6 @@ mod tests {
let envelope_json = assert_ok_string(&r); let envelope_json = assert_ok_string(&r);
regorus_result_drop(r); regorus_result_drop(r);
// Parse and verify structure
let envelope: serde_json::Value = let envelope: serde_json::Value =
serde_json::from_str(&envelope_json).expect("invalid JSON output"); serde_json::from_str(&envelope_json).expect("invalid JSON output");
assert!( assert!(
@@ -373,16 +487,13 @@ mod tests {
"envelope missing 'context'" "envelope missing 'context'"
); );
// The normalized resource should have lowercased alias fields
let res = &envelope["resource"]; let res = &envelope["resource"];
assert_eq!(res["supportshttpstrafficonly"], true); assert_eq!(res["supportshttpstrafficonly"], true);
assert_eq!(res["name"], "acct1"); assert_eq!(res["name"], "acct1");
// Context and parameters should be passed through
assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1"); assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1");
assert_eq!(envelope["parameters"]["env"], "prod"); assert_eq!(envelope["parameters"]["env"], "prod");
// Denormalize the resource portion
let resource_json = serde_json::to_string(&res).expect("serialize resource"); let resource_json = serde_json::to_string(&res).expect("serialize resource");
let norm_cstr = c(&resource_json); let norm_cstr = c(&resource_json);
@@ -392,7 +503,6 @@ mod tests {
let denorm: serde_json::Value = let denorm: serde_json::Value =
serde_json::from_str(&denorm_json).expect("invalid denorm JSON"); serde_json::from_str(&denorm_json).expect("invalid denorm JSON");
// Should be back under properties with restored casing
assert_eq!( assert_eq!(
denorm["properties"]["supportsHttpsTrafficOnly"], true, denorm["properties"]["supportsHttpsTrafficOnly"], true,
"expected restored casing under properties" "expected restored casing under properties"
@@ -403,11 +513,7 @@ mod tests {
#[test] #[test]
fn denormalize_invalid_json_returns_error() { fn denormalize_invalid_json_returns_error() {
let reg = regorus_alias_registry_new(); let reg = build_registry_with_json(ALIASES);
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 bad = c("not json");
let api = c("2023-01-01"); let api = c("2023-01-01");
@@ -420,11 +526,7 @@ mod tests {
#[test] #[test]
fn normalize_data_plane_manifest() { fn normalize_data_plane_manifest() {
let reg = regorus_alias_registry_new(); let reg = build_registry_with_manifest(MANIFEST);
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#"{ let resource = c(r#"{
"type": "Microsoft.KeyVault.Data/vaults/certificates", "type": "Microsoft.KeyVault.Data/vaults/certificates",
@@ -453,7 +555,12 @@ mod tests {
#[test] #[test]
fn empty_registry_normalize() { fn empty_registry_normalize() {
let reg = regorus_alias_registry_new(); let builder = regorus_alias_registry_builder_new();
let r = regorus_alias_registry_builder_build(builder);
let reg = assert_ok_pointer(&r) as *mut RegorusAliasRegistry;
regorus_result_drop(r);
regorus_alias_registry_builder_drop(builder);
let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#); let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#);
let api = c(""); let api = c("");
let ctx = c("{}"); let ctx = c("{}");
@@ -470,7 +577,6 @@ mod tests {
regorus_result_drop(r); regorus_result_drop(r);
let envelope: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON"); 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"]["foo"], 1);
assert_eq!(envelope["resource"]["name"], "test"); assert_eq!(envelope["resource"]["name"], "test");
+4
View File
@@ -236,6 +236,10 @@ pub(crate) fn to_ref<'a, T>(t: *mut T) -> Result<&'a mut T> {
unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) } unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) }
} }
pub(crate) fn to_shared_ref<'a, T>(t: *const T) -> Result<&'a T> {
unsafe { t.as_ref().ok_or_else(|| anyhow!("null pointer")) }
}
pub(crate) fn to_regorus_result(r: Result<()>) -> RegorusResult { pub(crate) fn to_regorus_result(r: Result<()>) -> RegorusResult {
match r { match r {
Ok(()) => RegorusResult::ok_void(), Ok(()) => RegorusResult::ok_void(),
+614 -1
View File
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
use crate::common::{from_c_str, RegorusResult, RegorusStatus}; use crate::common::{from_c_str, to_shared_ref, RegorusResult, RegorusStatus};
use crate::compiled_policy::RegorusCompiledPolicy; use crate::compiled_policy::RegorusCompiledPolicy;
use crate::panic_guard::with_unwind_guard; use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box; use alloc::boxed::Box;
@@ -208,6 +208,220 @@ fn convert_c_modules_to_rust(
Ok(policy_modules) Ok(policy_modules)
} }
// ---------------------------------------------------------------------------
// Azure Policy JSON compilation
// ---------------------------------------------------------------------------
/// Compile an Azure Policy JSON policy rule into an RVM program.
///
/// Parses the JSON `policyRule` (the `{ "if": ..., "then": ... }` object),
/// resolves aliases using the provided registry, and compiles the result
/// into an RVM [`Program`] that can be loaded into a [`RegorusRvm`].
///
/// # Parameters
/// * `registry` - Alias registry handle, or null.
/// * `policy_rule_json` - JSON string containing the policyRule object
///
/// # Null registry behavior
///
/// When `registry` is null, compilation proceeds **without alias resolution**.
/// Field references that correspond to Azure resource provider aliases
/// (e.g. `Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly`) will
/// be compiled as raw property paths rather than being resolved to their
/// short forms. This means:
///
/// - Policies that rely on aliases will **silently produce incorrect
/// evaluation results** because the field paths won't match the
/// normalized resource structure.
/// - **Modify / Append** effect policies will **skip the modifiability
/// validation** that normally rejects writes to non-modifiable aliases
/// at compile time.
///
/// Pass null only when the policy is known to contain no alias references
/// (e.g. simple `type` / `location` checks, or in unit-test scenarios).
///
/// # Returns
/// Returns a `RegorusResult` containing a `RegorusProgram` pointer on success.
///
/// # Safety
/// `policy_rule_json` must be a valid null-terminated UTF-8 string.
/// If `registry` is non-null it must be a valid `RegorusAliasRegistry` pointer.
/// The caller must eventually call `regorus_program_drop` on the returned handle.
#[cfg(all(feature = "azure_policy", feature = "rvm"))]
#[no_mangle]
pub extern "C" fn regorus_compile_azure_policy_rule(
registry: *const crate::alias_registry::RegorusAliasRegistry,
policy_rule_json: *const c_char,
) -> RegorusResult {
use crate::alias_registry::RegorusAliasRegistry;
use crate::rvm::RegorusProgram;
use alloc::sync::Arc;
use regorus::languages::azure_policy::{compiler, parser};
use regorus::Rc;
use regorus::Source;
with_unwind_guard(|| {
let result = || -> Result<RegorusProgram, (RegorusStatus, alloc::string::String)> {
let json_str = from_c_str(policy_rule_json).map_err(|e| {
(
RegorusStatus::InvalidDataFormat,
format!("Invalid policy rule JSON string: {e}"),
)
})?;
let source = Source::from_contents("policy_rule".into(), json_str).map_err(|e| {
(
RegorusStatus::InvalidDataFormat,
format!("Failed to create source: {e}"),
)
})?;
let ast = parser::parse_policy_rule(&source).map_err(|e| {
(
RegorusStatus::InvalidPolicy,
format!("Failed to parse policy rule: {e}"),
)
})?;
let program = if registry.is_null() {
compiler::compile_policy_rule(&ast)
} else {
let reg: &RegorusAliasRegistry = to_shared_ref(registry).map_err(|e| {
(
RegorusStatus::InvalidArgument,
format!("Invalid alias registry: {e}"),
)
})?;
compiler::compile_policy_rule_with_aliases(&ast, reg.inner())
};
program
.map(|p| RegorusProgram {
program: Arc::new(Rc::try_unwrap(p).unwrap_or_else(|rc| (*rc).clone())),
})
.map_err(|e| {
(
RegorusStatus::CompilationFailed,
format!("Failed to compile policy rule: {e}"),
)
})
}();
match result {
Ok(program) => {
RegorusResult::ok_pointer(Box::into_raw(Box::new(program)) as *mut c_void)
}
Err((status, msg)) => RegorusResult::err_with_message(status, msg),
}
})
}
/// Compile a full Azure Policy definition JSON into an RVM program.
///
/// Parses the JSON policy definition (which includes `policyRule`, `parameters`,
/// `displayName`, etc.), resolves aliases using the provided registry, and
/// compiles the result into an RVM [`Program`].
///
/// The definition JSON may be in either wrapped or unwrapped form:
/// - **Wrapped**: `{ "properties": { "policyRule": ..., "parameters": ... }, "id": ... }`
/// - **Unwrapped**: `{ "policyRule": ..., "parameters": ..., "displayName": ... }`
///
/// # Parameters
/// * `registry` - Alias registry handle, or null.
/// * `policy_definition_json` - JSON string containing the full policy definition
///
/// # Null registry behavior
///
/// When `registry` is null, compilation proceeds **without alias resolution**.
/// Field references that correspond to Azure resource provider aliases will
/// be compiled as raw property paths rather than being resolved. This means:
///
/// - Policies that rely on aliases will **silently produce incorrect
/// evaluation results**.
/// - **Modify / Append** effect policies will **skip the modifiability
/// validation** that normally rejects writes to non-modifiable aliases
/// at compile time.
///
/// Pass null only when the policy is known to contain no alias references
/// (e.g. simple `type` / `location` checks, or in unit-test scenarios).
///
/// # Returns
/// Returns a `RegorusResult` containing a `RegorusProgram` pointer on success.
///
/// # Safety
/// `policy_definition_json` must be a valid null-terminated UTF-8 string.
/// If `registry` is non-null it must be a valid `RegorusAliasRegistry` pointer.
/// The caller must eventually call `regorus_program_drop` on the returned handle.
#[cfg(all(feature = "azure_policy", feature = "rvm"))]
#[no_mangle]
pub extern "C" fn regorus_compile_azure_policy_definition(
registry: *const crate::alias_registry::RegorusAliasRegistry,
policy_definition_json: *const c_char,
) -> RegorusResult {
use crate::alias_registry::RegorusAliasRegistry;
use crate::rvm::RegorusProgram;
use alloc::sync::Arc;
use regorus::languages::azure_policy::{compiler, parser};
use regorus::Rc;
use regorus::Source;
with_unwind_guard(|| {
let result = || -> Result<RegorusProgram, (RegorusStatus, alloc::string::String)> {
let json_str = from_c_str(policy_definition_json).map_err(|e| {
(
RegorusStatus::InvalidDataFormat,
format!("Invalid policy definition JSON string: {e}"),
)
})?;
let source =
Source::from_contents("policy_definition".into(), json_str).map_err(|e| {
(
RegorusStatus::InvalidDataFormat,
format!("Failed to create source: {e}"),
)
})?;
let defn = parser::parse_policy_definition(&source).map_err(|e| {
(
RegorusStatus::InvalidPolicy,
format!("Failed to parse policy definition: {e}"),
)
})?;
let program = if registry.is_null() {
compiler::compile_policy_definition(&defn)
} else {
let reg: &RegorusAliasRegistry = to_shared_ref(registry).map_err(|e| {
(
RegorusStatus::InvalidArgument,
format!("Invalid alias registry: {e}"),
)
})?;
compiler::compile_policy_definition_with_aliases(&defn, reg.inner())
};
program
.map(|p| RegorusProgram {
program: Arc::new(Rc::try_unwrap(p).unwrap_or_else(|rc| (*rc).clone())),
})
.map_err(|e| {
(
RegorusStatus::CompilationFailed,
format!("Failed to compile policy definition: {e}"),
)
})
}();
match result {
Ok(program) => {
RegorusResult::ok_pointer(Box::into_raw(Box::new(program)) as *mut c_void)
}
Err((status, msg)) => RegorusResult::err_with_message(status, msg),
}
})
}
#[cfg(feature = "std")] #[cfg(feature = "std")]
fn report_module_error(index: usize, kind: &str, err: &anyhow::Error) { fn report_module_error(index: usize, kind: &str, err: &anyhow::Error) {
eprintln!("Invalid {} at index {}: {}", kind, index, err); eprintln!("Invalid {} at index {}: {}", kind, index, err);
@@ -215,3 +429,402 @@ fn report_module_error(index: usize, kind: &str, err: &anyhow::Error) {
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
fn report_module_error(_index: usize, _kind: &str, _err: &anyhow::Error) {} fn report_module_error(_index: usize, _kind: &str, _err: &anyhow::Error) {}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::common::regorus_result_drop;
use core::ffi::CStr;
use std::ffi::CString;
fn c(s: &str) -> CString {
CString::new(s).expect("CString::new failed")
}
fn assert_ok_pointer(r: &RegorusResult) -> *mut c_void {
assert_eq!(
r.status,
RegorusStatus::Ok,
"expected Ok, got {:?}",
r.status
);
assert!(!r.pointer_value.is_null(), "expected non-null pointer");
r.pointer_value
}
#[cfg(all(feature = "azure_policy", feature = "rvm"))]
mod azure_policy_json {
use super::*;
use crate::alias_registry::regorus_alias_registry_drop;
use crate::rvm::{
regorus_program_drop, regorus_rvm_drop, regorus_rvm_execute_entry_point_by_name,
regorus_rvm_load_program, regorus_rvm_new, regorus_rvm_set_context,
regorus_rvm_set_input, RegorusProgram,
};
const ALIASES: &str = r#"[{
"namespace": "Microsoft.Storage",
"resourceTypes": [{
"resourceType": "storageAccounts",
"aliases": [{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
}, {
"name": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
"defaultPath": "properties.minimumTlsVersion",
"paths": []
}]
}]
}]"#;
const SIMPLE_POLICY_RULE: &str = r#"{
"if": {
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
},
"then": { "effect": "audit" }
}"#;
const ALIAS_POLICY_RULE: &str = r#"{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false }
]
},
"then": { "effect": "deny" }
}"#;
const POLICY_DEFINITION: &str = r#"{
"displayName": "Require HTTPS for storage accounts",
"policyType": "Custom",
"mode": "Indexed",
"parameters": {
"effect": {
"type": "String",
"defaultValue": "deny"
}
},
"policyRule": {
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false }
]
},
"then": { "effect": "[parameters('effect')]" }
}
}"#;
/// Wrap a normalized resource JSON into the input envelope expected by
/// the compiled Azure Policy RVM program.
fn wrap_input(resource_json: &str, parameters_json: &str) -> String {
format!(r#"{{"resource": {resource_json}, "parameters": {parameters_json}}}"#)
}
fn build_registry_with_json(
json: &str,
) -> *mut crate::alias_registry::RegorusAliasRegistry {
let builder = crate::alias_registry::regorus_alias_registry_builder_new();
let json_c = c(json);
let r = crate::alias_registry::regorus_alias_registry_builder_load_json(
builder,
json_c.as_ptr(),
);
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = crate::alias_registry::regorus_alias_registry_builder_build(builder);
let registry =
assert_ok_pointer(&r) as *mut crate::alias_registry::RegorusAliasRegistry;
regorus_result_drop(r);
crate::alias_registry::regorus_alias_registry_builder_drop(builder);
registry
}
/// Helper: compile a policy rule, execute it with input, and return the
/// result string.
unsafe fn compile_and_eval_rule(
registry: *const crate::alias_registry::RegorusAliasRegistry,
policy_rule: &str,
input_json: &str,
) -> String {
let rule_c = c(policy_rule);
let r = regorus_compile_azure_policy_rule(registry, rule_c.as_ptr());
let program_ptr = assert_ok_pointer(&r) as *mut RegorusProgram;
regorus_result_drop(r);
let vm = regorus_rvm_new();
assert!(!vm.is_null());
let r = regorus_rvm_load_program(vm, program_ptr);
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let input_c = c(input_json);
let r = regorus_rvm_set_input(vm, input_c.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let entry = c("main");
let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok, "execute failed");
let output = CStr::from_ptr(r.output)
.to_str()
.expect("invalid UTF-8")
.to_string();
regorus_result_drop(r);
regorus_rvm_drop(vm);
regorus_program_drop(program_ptr);
output
}
#[test]
fn compile_simple_rule_no_aliases() {
let rule_c = c(SIMPLE_POLICY_RULE);
let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_c.as_ptr());
let ptr = assert_ok_pointer(&r);
regorus_result_drop(r);
regorus_program_drop(ptr as *mut RegorusProgram);
}
#[test]
fn compile_rule_with_aliases() {
let reg = build_registry_with_json(ALIASES);
let rule_c = c(ALIAS_POLICY_RULE);
let r = regorus_compile_azure_policy_rule(reg, rule_c.as_ptr());
let ptr = assert_ok_pointer(&r);
regorus_result_drop(r);
regorus_program_drop(ptr as *mut RegorusProgram);
regorus_alias_registry_drop(reg);
}
#[test]
fn compile_and_eval_simple_rule_matching() {
let input = wrap_input(r#"{"type":"microsoft.storage/storageaccounts"}"#, "{}");
let result =
unsafe { compile_and_eval_rule(core::ptr::null_mut(), SIMPLE_POLICY_RULE, &input) };
let parsed: serde_json::Value =
serde_json::from_str(&result).expect("result should be valid JSON");
assert_eq!(
parsed["effect"], "audit",
"expected audit effect, got: {result}"
);
}
#[test]
fn compile_and_eval_simple_rule_not_matching() {
let input = wrap_input(r#"{"type":"microsoft.compute/virtualmachines"}"#, "{}");
let result =
unsafe { compile_and_eval_rule(core::ptr::null_mut(), SIMPLE_POLICY_RULE, &input) };
// When the "if" condition doesn't match, the result should be undefined
assert!(
result.contains("undefined"),
"expected undefined for non-matching input, got: {result}"
);
}
#[test]
fn compile_and_eval_alias_rule_deny() {
let reg = build_registry_with_json(ALIASES);
// Non-compliant resource: HTTPS not enabled (normalized form)
let input = wrap_input(
r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": false}"#,
"{}",
);
let result = unsafe { compile_and_eval_rule(reg, ALIAS_POLICY_RULE, &input) };
let parsed: serde_json::Value = serde_json::from_str(&result).expect("valid JSON");
assert_eq!(parsed["effect"], "deny", "expected deny, got: {result}");
regorus_alias_registry_drop(reg);
}
#[test]
fn compile_and_eval_alias_rule_compliant() {
let reg = build_registry_with_json(ALIASES);
// Compliant resource: HTTPS enabled (normalized form)
let input = wrap_input(
r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": true}"#,
"{}",
);
let result = unsafe { compile_and_eval_rule(reg, ALIAS_POLICY_RULE, &input) };
assert!(
result.contains("undefined"),
"expected undefined for compliant resource, got: {result}"
);
regorus_alias_registry_drop(reg);
}
#[test]
fn compile_definition_no_aliases() {
let defn_c = c(POLICY_DEFINITION);
let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), defn_c.as_ptr());
let ptr = assert_ok_pointer(&r);
regorus_result_drop(r);
regorus_program_drop(ptr as *mut RegorusProgram);
}
#[test]
fn compile_definition_with_aliases_and_eval() {
let reg = build_registry_with_json(ALIASES);
let defn_c = c(POLICY_DEFINITION);
let r = regorus_compile_azure_policy_definition(reg, defn_c.as_ptr());
let program_ptr = assert_ok_pointer(&r) as *mut RegorusProgram;
regorus_result_drop(r);
// Evaluate with a non-compliant resource (normalized form, wrapped in envelope)
unsafe {
let vm = regorus_rvm_new();
let r = regorus_rvm_load_program(vm, program_ptr);
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let input_json = wrap_input(
r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": false}"#,
"{}",
);
let input = c(&input_json);
let r = regorus_rvm_set_input(vm, input.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let entry = c("main");
let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
let result = CStr::from_ptr(r.output)
.to_str()
.expect("UTF-8")
.to_string();
regorus_result_drop(r);
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
// The default parameter value is "deny"
assert_eq!(parsed["effect"], "deny", "got: {result}");
regorus_rvm_drop(vm);
regorus_program_drop(program_ptr);
}
regorus_alias_registry_drop(reg);
}
#[test]
fn invalid_json_returns_error() {
let bad = c("not valid json");
let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), bad.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
}
#[test]
fn invalid_definition_returns_error() {
let bad = c(r#"{"not": "a policy definition"}"#);
let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), bad.as_ptr());
assert_ne!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
}
/// Policy rule that uses a context function (subscription()).
const CONTEXT_POLICY_RULE: &str = r#"{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "value": "[subscription().subscriptionId]", "equals": "sub-123" }
]
},
"then": { "effect": "deny" }
}"#;
#[test]
fn context_policy_evaluates_with_set_context() {
let rule_c = c(CONTEXT_POLICY_RULE);
let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_c.as_ptr());
let program = assert_ok_pointer(&r) as *mut RegorusProgram;
regorus_result_drop(r);
let vm = regorus_rvm_new();
assert!(!vm.is_null());
let r = regorus_rvm_load_program(vm, program);
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
// Set the context with subscription info
let context = c(r#"{"subscription": {"subscriptionId": "sub-123"}}"#);
let r = regorus_rvm_set_context(vm, context.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
// Set matching input
let input = c(&wrap_input(
r#"{"type": "microsoft.storage/storageaccounts"}"#,
"{}",
));
let r = regorus_rvm_set_input(vm, input.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let entry = c("main");
let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
let output = unsafe { CStr::from_ptr(r.output) }.to_str().unwrap();
assert!(
output.contains("deny"),
"expected deny effect with matching context, got: {output}"
);
regorus_result_drop(r);
regorus_rvm_drop(vm);
regorus_program_drop(program);
}
#[test]
fn context_policy_undefined_without_context() {
let rule_c = c(CONTEXT_POLICY_RULE);
let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_c.as_ptr());
let program = assert_ok_pointer(&r) as *mut RegorusProgram;
regorus_result_drop(r);
let vm = regorus_rvm_new();
assert!(!vm.is_null());
let r = regorus_rvm_load_program(vm, program);
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
// No context set — subscription() will be undefined
let input = c(&wrap_input(
r#"{"type": "microsoft.storage/storageaccounts"}"#,
"{}",
));
let r = regorus_rvm_set_input(vm, input.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let entry = c("main");
let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
let output = unsafe { CStr::from_ptr(r.output) }.to_str().unwrap();
assert!(
output.contains("undefined"),
"expected undefined without context, got: {output}"
);
regorus_result_drop(r);
regorus_rvm_drop(vm);
regorus_program_drop(program);
}
}
}
+4 -2
View File
@@ -39,7 +39,7 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input(
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?; let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?;
let result = to_ref(compiled_policy)? let result = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?
.compiled_policy .compiled_policy
.eval_with_input(input_value)?; .eval_with_input(input_value)?;
result.to_json_str() result.to_json_str()
@@ -65,7 +65,9 @@ pub extern "C" fn regorus_compiled_policy_get_policy_info(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?; let info = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?
.compiled_policy
.get_policy_info()?;
serde_json::to_string(&info) serde_json::to_string(&info)
.map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e)) .map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e))
}(); }();
+52 -47
View File
@@ -2,7 +2,8 @@
// Licensed under the MIT License. // Licensed under the MIT License.
use crate::common::{ use crate::common::{
from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus, from_c_str, to_ref, to_regorus_result, to_regorus_string_result, to_shared_ref, RegorusResult,
RegorusStatus,
}; };
use crate::compiled_policy::RegorusCompiledPolicy; use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig; use crate::limits::RegorusExecutionTimerConfig;
@@ -193,7 +194,7 @@ pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine {
/// ///
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine { pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine {
match to_ref(engine) { match to_shared_ref(engine as *const RegorusEngine) {
Ok(e) => Box::into_raw(Box::new(e.clone())), Ok(e) => Box::into_raw(Box::new(e.clone())),
_ => ptr::null_mut(), _ => ptr::null_mut(),
} }
@@ -223,7 +224,7 @@ pub extern "C" fn regorus_engine_add_policy(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> { to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.add_policy(from_c_str(path)?, from_c_str(rego)?) guard.add_policy(from_c_str(path)?, from_c_str(rego)?)
}()) }())
@@ -238,7 +239,7 @@ pub extern "C" fn regorus_engine_add_policy_from_file(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> { to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.add_policy_from_file(from_c_str(path)?) guard.add_policy_from_file(from_c_str(path)?)
}()) }())
@@ -256,7 +257,7 @@ pub extern "C" fn regorus_engine_add_data_json(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?) guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?)
}()) }())
@@ -270,7 +271,7 @@ pub extern "C" fn regorus_engine_add_data_json(
pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> { to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg) serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg)
}()) }())
@@ -284,7 +285,7 @@ pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> Reg
pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> { to_regorus_string_result(|| -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_policies_as_json() guard.get_policies_as_json()
}()) }())
@@ -299,7 +300,7 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?) guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?)
}()) }())
@@ -313,7 +314,7 @@ pub extern "C" fn regorus_engine_add_data_from_json_file(
pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_data(); guard.clear_data();
Ok(()) Ok(())
@@ -332,7 +333,7 @@ pub extern "C" fn regorus_engine_set_input_json(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?); guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?);
Ok(()) Ok(())
@@ -348,7 +349,7 @@ pub extern "C" fn regorus_engine_set_input_from_json_file(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?); guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?);
Ok(()) Ok(())
@@ -367,7 +368,7 @@ pub extern "C" fn regorus_engine_eval_query(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
let results = guard.eval_query(from_c_str(query)?, false)?; let results = guard.eval_query(from_c_str(query)?, false)?;
Ok(serde_json::to_string_pretty(&results)?) Ok(serde_json::to_string_pretty(&results)?)
@@ -390,7 +391,7 @@ pub extern "C" fn regorus_engine_eval_rule(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.eval_rule(from_c_str(rule)?)?.to_json_str() guard.eval_rule(from_c_str(rule)?)?.to_json_str()
}(); }();
@@ -413,7 +414,7 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable); guard.set_enable_coverage(enable);
Ok(()) Ok(())
@@ -429,7 +430,7 @@ pub extern "C" fn regorus_engine_set_enable_coverage(
pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?) Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?)
}(); }();
@@ -451,7 +452,7 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_strict_builtin_errors(strict); guard.set_strict_builtin_errors(strict);
Ok(()) Ok(())
@@ -465,18 +466,20 @@ pub extern "C" fn regorus_engine_set_execution_timer_config(
engine: *mut RegorusEngine, engine: *mut RegorusEngine,
config: *const RegorusExecutionTimerConfig, config: *const RegorusExecutionTimerConfig,
) -> RegorusResult { ) -> RegorusResult {
to_regorus_result(|| -> Result<()> { with_unwind_guard(|| {
let engine = to_ref(engine)?; to_regorus_result(|| -> Result<()> {
let config = unsafe { let engine = to_shared_ref(engine as *const RegorusEngine)?;
config let config = unsafe {
.as_ref() config
.copied() .as_ref()
.ok_or_else(|| anyhow!("execution timer config pointer is null"))? .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()?); let mut guard = engine.try_write()?;
Ok(()) guard.set_execution_timer_config(config.to_execution_timer_config()?);
}()) Ok(())
}())
})
} }
#[no_mangle] #[no_mangle]
@@ -484,12 +487,14 @@ pub extern "C" fn regorus_engine_set_execution_timer_config(
pub extern "C" fn regorus_engine_clear_execution_timer_config( pub extern "C" fn regorus_engine_clear_execution_timer_config(
engine: *mut RegorusEngine, engine: *mut RegorusEngine,
) -> RegorusResult { ) -> RegorusResult {
to_regorus_result(|| -> Result<()> { with_unwind_guard(|| {
let engine = to_ref(engine)?; to_regorus_result(|| -> Result<()> {
let mut guard = engine.try_write()?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
guard.clear_execution_timer_config(); let mut guard = engine.try_write()?;
Ok(()) guard.clear_execution_timer_config();
}()) Ok(())
}())
})
} }
/// Set the policy length limits used when loading policies. /// Set the policy length limits used when loading policies.
@@ -500,7 +505,7 @@ pub extern "C" fn regorus_engine_set_policy_length_config(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_policy_length_config(config.to_policy_length_config()?); guard.set_policy_length_config(config.to_policy_length_config()?);
Ok(()) Ok(())
@@ -515,7 +520,7 @@ pub extern "C" fn regorus_engine_clear_policy_length_config(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_policy_length_config(); guard.clear_policy_length_config();
Ok(()) Ok(())
@@ -533,7 +538,7 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty() guard.get_coverage_report()?.to_string_pretty()
}(); }();
@@ -552,7 +557,7 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty(
pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_coverage_data(); guard.clear_coverage_data();
Ok(()) Ok(())
@@ -571,7 +576,7 @@ pub extern "C" fn regorus_engine_set_gather_prints(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_gather_prints(enable); guard.set_gather_prints(enable);
Ok(()) Ok(())
@@ -586,7 +591,7 @@ pub extern "C" fn regorus_engine_set_gather_prints(
pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
Ok(serde_json::to_string_pretty(&guard.take_prints()?)?) Ok(serde_json::to_string_pretty(&guard.take_prints()?)?)
}(); }();
@@ -605,7 +610,7 @@ pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> Rego
pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_ast_as_json() guard.get_ast_as_json()
}(); }();
@@ -626,7 +631,7 @@ pub extern "C" fn regorus_engine_get_policy_package_names(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_package_names()?) serde_json::to_string_pretty(&guard.get_policy_package_names()?)
.map_err(anyhow::Error::msg) .map_err(anyhow::Error::msg)
@@ -648,7 +653,7 @@ pub extern "C" fn regorus_engine_get_policy_parameters(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
serde_json::to_string_pretty(&guard.get_policy_parameters()?) serde_json::to_string_pretty(&guard.get_policy_parameters()?)
.map_err(anyhow::Error::msg) .map_err(anyhow::Error::msg)
@@ -670,7 +675,7 @@ pub extern "C" fn regorus_engine_set_rego_v0(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<()> { let output = || -> Result<()> {
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_rego_v0(enable); guard.set_rego_v0(enable);
Ok(()) Ok(())
@@ -692,7 +697,7 @@ pub extern "C" fn regorus_engine_set_rego_v0(
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult { pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let engine = match to_ref(engine) { let engine = match to_shared_ref(engine as *const RegorusEngine) {
Ok(engine) => engine, Ok(engine) => engine,
Err(e) => { Err(e) => {
return RegorusResult::err_with_message( return RegorusResult::err_with_message(
@@ -741,7 +746,7 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint(
let result = || -> Result<RegorusCompiledPolicy> { let result = || -> Result<RegorusCompiledPolicy> {
let rule_str = from_c_str(rule)?; let rule_str = from_c_str(rule)?;
let rule_rc: regorus::Rc<str> = rule_str.into(); let rule_rc: regorus::Rc<str> = rule_str.into();
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
Ok(RegorusCompiledPolicy { compiled_policy }) Ok(RegorusCompiledPolicy { compiled_policy })
@@ -800,7 +805,7 @@ pub extern "C" fn regorus_engine_compile_program_with_entrypoints(
.ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?; .ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?;
let rule_rc: regorus::Rc<str> = (*rule).into(); let rule_rc: regorus::Rc<str> = (*rule).into();
let engine = to_ref(engine)?; let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?;
+57 -21
View File
@@ -2,7 +2,8 @@
// Licensed under the MIT License. // Licensed under the MIT License.
use crate::common::{ use crate::common::{
from_c_str, to_ref, to_regorus_result, RegorusBuffer, RegorusResult, RegorusStatus, from_c_str, to_ref, to_regorus_result, to_shared_ref, RegorusBuffer, RegorusResult,
RegorusStatus,
}; };
use crate::compile::RegorusPolicyModule; use crate::compile::RegorusPolicyModule;
use crate::compiled_policy::RegorusCompiledPolicy; use crate::compiled_policy::RegorusCompiledPolicy;
@@ -106,7 +107,8 @@ pub extern "C" fn regorus_program_compile_from_policy(
let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); 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 compiled_policy =
&to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?.compiled_policy;
let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?; let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?;
Ok(Box::into_raw(Box::new(RegorusProgram { program }))) Ok(Box::into_raw(Box::new(RegorusProgram { program })))
}(); }();
@@ -187,7 +189,7 @@ pub extern "C" fn regorus_program_new() -> *mut RegorusProgram {
pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult { pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<*mut RegorusBuffer> { let output = || -> Result<*mut RegorusBuffer> {
let program = &to_ref(program)?.program; let program = &to_shared_ref(program as *const RegorusProgram)?.program;
let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?; let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?;
Ok(RegorusBuffer::from_vec(bytes)) Ok(RegorusBuffer::from_vec(bytes))
}(); }();
@@ -211,7 +213,10 @@ pub extern "C" fn regorus_program_deserialize_binary(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<(*mut RegorusProgram, bool)> { let output = || -> Result<(*mut RegorusProgram, bool)> {
if data.is_null() && len > 0 { if data.is_null() {
if len > 0 {
return Err(anyhow!("null data pointer with non-zero length"));
}
return Err(anyhow!("null data pointer")); return Err(anyhow!("null data pointer"));
} }
let data = unsafe { core::slice::from_raw_parts(data, len) }; let data = unsafe { core::slice::from_raw_parts(data, len) };
@@ -249,7 +254,7 @@ pub extern "C" fn regorus_program_deserialize_binary(
pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult { pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let program = &to_ref(program)?.program; let program = &to_shared_ref(program as *const RegorusProgram)?.program;
Ok(generate_assembly_listing( Ok(generate_assembly_listing(
program, program,
&AssemblyListingConfig::default(), &AssemblyListingConfig::default(),
@@ -270,7 +275,7 @@ pub extern "C" fn regorus_program_generate_tabular_listing(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let program = &to_ref(program)?.program; let program = &to_shared_ref(program as *const RegorusProgram)?.program;
Ok(generate_tabular_assembly_listing( Ok(generate_tabular_assembly_listing(
program, program,
&AssemblyListingConfig::default(), &AssemblyListingConfig::default(),
@@ -297,7 +302,9 @@ pub extern "C" fn regorus_rvm_new_with_policy(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<*mut RegorusRvm> { let output = || -> Result<*mut RegorusRvm> {
let policy = to_ref(compiled_policy)?.compiled_policy.clone(); let policy = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?
.compiled_policy
.clone();
Ok(Box::into_raw(Box::new(RegorusRvm::new( Ok(Box::into_raw(Box::new(RegorusRvm::new(
RegoVM::new_with_policy(policy), RegoVM::new_with_policy(policy),
)))) ))))
@@ -318,9 +325,11 @@ pub extern "C" fn regorus_rvm_load_program(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let program = to_ref(program)?.program.clone(); let program = to_shared_ref(program as *const RegorusProgram)?
.program
.clone();
guard.load_program(program); guard.load_program(program);
Ok(()) Ok(())
}()) }())
@@ -332,7 +341,7 @@ pub extern "C" fn regorus_rvm_load_program(
pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult { pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let data_value = Value::from_json_str(&from_c_str(data)?)?; let data_value = Value::from_json_str(&from_c_str(data)?)?;
guard.set_data(data_value)?; guard.set_data(data_value)?;
@@ -349,7 +358,7 @@ pub extern "C" fn regorus_rvm_set_input(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let input_value = Value::from_json_str(&from_c_str(input)?)?; let input_value = Value::from_json_str(&from_c_str(input)?)?;
guard.set_input(input_value); guard.set_input(input_value);
@@ -358,6 +367,33 @@ pub extern "C" fn regorus_rvm_set_input(
}) })
} }
/// Set the VM context document from JSON.
///
/// The context provides host-supplied ambient data (e.g. `resourceGroup()`,
/// `subscription()`) that Azure Policy functions can access via `LoadContext`
/// instructions. This must be called before `regorus_rvm_execute` when
/// evaluating policies that reference context functions.
///
/// # Safety
/// - `vm` must be a valid pointer to a `RegorusRvm` created by `regorus_rvm_new`.
/// - `context_json` must be a valid null-terminated UTF-8 string.
#[cfg(feature = "azure_policy")]
#[no_mangle]
pub extern "C" fn regorus_rvm_set_context(
vm: *mut RegorusRvm,
context_json: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?;
let context_value = Value::from_json_str(&from_c_str(context_json)?)?;
guard.set_context(context_value);
Ok(())
}())
})
}
/// Set the maximum number of instructions that can execute. /// Set the maximum number of instructions that can execute.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_rvm_set_max_instructions( pub extern "C" fn regorus_rvm_set_max_instructions(
@@ -366,7 +402,7 @@ pub extern "C" fn regorus_rvm_set_max_instructions(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
guard.set_max_instructions(max_instructions); guard.set_max_instructions(max_instructions);
Ok(()) Ok(())
@@ -382,7 +418,7 @@ pub extern "C" fn regorus_rvm_set_strict_builtin_errors(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
guard.set_strict_builtin_errors(strict); guard.set_strict_builtin_errors(strict);
Ok(()) Ok(())
@@ -395,7 +431,7 @@ pub extern "C" fn regorus_rvm_set_strict_builtin_errors(
pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult { pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let mode = match mode { let mode = match mode {
0 => ExecutionMode::RunToCompletion, 0 => ExecutionMode::RunToCompletion,
@@ -413,7 +449,7 @@ pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8)
pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult { pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
guard.set_step_mode(enabled); guard.set_step_mode(enabled);
Ok(()) Ok(())
@@ -430,7 +466,7 @@ pub extern "C" fn regorus_rvm_set_execution_timer_config(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> { to_regorus_result(|| -> Result<()> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
if has_config { if has_config {
guard.set_execution_timer_config(Some(config.to_execution_timer_config()?)); guard.set_execution_timer_config(Some(config.to_execution_timer_config()?));
@@ -447,7 +483,7 @@ pub extern "C" fn regorus_rvm_set_execution_timer_config(
pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult { pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let result = guard.execute()?; let result = guard.execute()?;
result.to_json_str() result.to_json_str()
@@ -468,7 +504,7 @@ pub extern "C" fn regorus_rvm_execute_entry_point_by_name(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let name = from_c_str(entry_point)?; let name = from_c_str(entry_point)?;
let result = guard.execute_entry_point_by_name(&name)?; let result = guard.execute_entry_point_by_name(&name)?;
@@ -490,7 +526,7 @@ pub extern "C" fn regorus_rvm_execute_entry_point_by_index(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let result = guard.execute_entry_point_by_index(index)?; let result = guard.execute_entry_point_by_index(index)?;
result.to_json_str() result.to_json_str()
@@ -512,7 +548,7 @@ pub extern "C" fn regorus_rvm_resume(
) -> RegorusResult { ) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let value = if has_value { let value = if has_value {
Some(Value::from_json_str(&from_c_str(resume_value_json)?)?) Some(Value::from_json_str(&from_c_str(resume_value_json)?)?)
@@ -535,7 +571,7 @@ pub extern "C" fn regorus_rvm_resume(
pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult { pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<String> { let output = || -> Result<String> {
let vm = to_ref(vm)?; let vm = to_shared_ref(vm as *const RegorusRvm)?;
let guard = vm.try_read()?; let guard = vm.try_read()?;
let state: ExecutionState = guard.execution_state().clone(); let state: ExecutionState = guard.execution_state().clone();
Ok(format!("{:?}", state)) Ok(format!("{:?}", state))
+31 -31
View File
@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -199,9 +199,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -598,9 +598,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -670,9 +670,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -682,9 +682,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1000,7 +1000,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1032,7 +1032,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-java" name = "regorus-java"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"jni", "jni",
@@ -1042,7 +1042,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.6" version = "2.2.7"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1128,9 +1128,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1194,9 +1194,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1360,9 +1360,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1373,9 +1373,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1383,9 +1383,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1396,9 +1396,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1639,18 +1639,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+2 -2
View File
@@ -2,7 +2,7 @@
[package] [package]
name = "regorus-java" name = "regorus-java"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/java" repository = "https://github.com/microsoft/regorus/bindings/java"
description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -21,6 +21,6 @@ cache = ["regorus/cache"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
serde_json = "1.0.112" serde_json = "1.0.150"
jni = "0.22.4" jni = "0.22.4"
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
+1 -1
View File
@@ -9,7 +9,7 @@
<groupId>com.microsoft.regorus</groupId> <groupId>com.microsoft.regorus</groupId>
<artifactId>regorus-java</artifactId> <artifactId>regorus-java</artifactId>
<version>0.10.0</version> <version>0.10.1</version>
<name>Regorus Java</name> <name>Regorus Java</name>
<description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description> <description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description>
+1 -1
View File
@@ -462,7 +462,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModul
} }
let mut modules = Vec::with_capacity(ids.len()); let mut modules = Vec::with_capacity(ids.len());
for (id, content) in ids.into_iter().zip(contents.into_iter()) { for (id, content) in ids.into_iter().zip(contents) {
modules.push(PolicyModule { modules.push(PolicyModule {
id: Rc::from(id.as_str()), id: Rc::from(id.as_str()),
content: Rc::from(content.as_str()), content: Rc::from(content.as_str()),
+41 -42
View File
@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -183,9 +183,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -533,9 +533,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -605,9 +605,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -617,9 +617,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -837,9 +837,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.28.3" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@@ -852,18 +852,18 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.28.3" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
dependencies = [ dependencies = [
"target-lexicon", "target-lexicon",
] ]
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.28.3" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@@ -871,9 +871,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.28.3" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -883,13 +883,12 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.28.3" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"pyo3-build-config",
"quote", "quote",
"syn", "syn",
] ]
@@ -1009,7 +1008,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1041,7 +1040,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.6" version = "2.2.7"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1055,7 +1054,7 @@ dependencies = [
[[package]] [[package]]
name = "regoruspy" name = "regoruspy"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ordered-float", "ordered-float",
@@ -1120,9 +1119,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1170,9 +1169,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1332,9 +1331,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1345,9 +1344,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1355,9 +1354,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1368,9 +1367,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1593,18 +1592,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+3 -3
View File
@@ -2,7 +2,7 @@
[package] [package]
name = "regoruspy" name = "regoruspy"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/python" repository = "https://github.com/microsoft/regorus/bindings/python"
description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -23,7 +23,7 @@ coverage = ["regorus/coverage"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
ordered-float = "5.3.0" ordered-float = "5.3.0"
pyo3 = { version = "0.28.3", features = ["abi3-py310", "anyhow", "extension-module"] } pyo3 = { version = "0.29.0", features = ["abi3-py310", "anyhow", "extension-module"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde_json = "1.0.140" serde_json = "1.0.150"
+33 -33
View File
@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
@@ -109,9 +109,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -212,9 +212,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -223,9 +223,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "email_address" name = "email_address"
@@ -571,9 +571,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -653,9 +653,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -688,9 +688,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1040,7 +1040,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1071,7 +1071,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.6" version = "2.2.7"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1085,7 +1085,7 @@ dependencies = [
[[package]] [[package]]
name = "regorusrb" name = "regorusrb"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"magnus", "magnus",
"regorus", "regorus",
@@ -1162,9 +1162,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1229,9 +1229,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1391,9 +1391,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1404,9 +1404,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1414,9 +1414,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1427,9 +1427,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1652,18 +1652,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "regorusrb" name = "regorusrb"
version = "0.10.0" version = "0.10.1"
edition = "2024" edition = "2024"
description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" description = "Ruby bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
license = "MIT AND Apache-2.0 AND BSD-3-Clause" license = "MIT AND Apache-2.0 AND BSD-3-Clause"
+1 -1
View File
@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Regorus module Regorus
VERSION = "0.10.0" VERSION = "0.10.1"
end end
+38 -38
View File
@@ -59,9 +59,9 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -102,9 +102,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -200,9 +200,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -565,9 +565,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -643,9 +643,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -655,9 +655,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -999,7 +999,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1030,7 +1030,7 @@ dependencies = [
[[package]] [[package]]
name = "regorusjs" name = "regorusjs"
version = "0.10.0" version = "0.10.1"
dependencies = [ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.17",
"getrandom 0.3.4", "getrandom 0.3.4",
@@ -1120,9 +1120,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1170,9 +1170,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.10.0" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1344,9 +1344,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1357,9 +1357,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.71" version = "0.4.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -1367,9 +1367,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1377,9 +1377,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1390,18 +1390,18 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"
version = "0.3.71" version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" checksum = "74fde991ccdc895cb7fbaa14b137d62af74d9011be67b71c694bfc40edd3119c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"cast", "cast",
@@ -1421,9 +1421,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-test-macro" name = "wasm-bindgen-test-macro"
version = "0.3.71" version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" checksum = "e925354648d2a4d1bf205412e36d520a800280622eef4719678d268e5d40e978"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1432,9 +1432,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-test-shared" name = "wasm-bindgen-test-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" checksum = "684365b586a9a6256c1cc3544eee8680de48d6041142f581776ec7b139622ae9"
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
@@ -1672,18 +1672,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+3 -3
View File
@@ -2,7 +2,7 @@
[package] [package]
name = "regorusjs" name = "regorusjs"
version = "0.10.0" version = "0.10.1"
edition = "2021" edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/wasm" repository = "https://github.com/microsoft/regorus/bindings/wasm"
description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
@@ -42,7 +42,7 @@ coverage = ["regorus/coverage"]
[dependencies] [dependencies]
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.150"
wasm-bindgen = "0.2.100" wasm-bindgen = "0.2.100"
serde-wasm-bindgen = "0.6" serde-wasm-bindgen = "0.6"
# Specify uuid as a mandatory dependency so as to enable `js` feature which is now required # Specify uuid as a mandatory dependency so as to enable `js` feature which is now required
@@ -55,7 +55,7 @@ getrandom03 = { package = "getrandom", version = "0.3.1", features = ["std", "wa
getrandom = { version = "0.4.2", features = ["wasm_js"] } getrandom = { version = "0.4.2", features = ["wasm_js"] }
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = "0.3.71" wasm-bindgen-test = "0.3.72"
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] } unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }
+84
View File
@@ -0,0 +1,84 @@
# Object
Opaque container for `Value::Object`'s key→value storage, enabling
alternative backends without call-site changes.
## Design
`Object` wraps the storage for a key→value collection of `Value`s and
provides a curated set of methods (`get`, `insert`, `remove`, `iter`,
`iter_sorted`, `cursor`, serde). The backing store is private; callers
never see or pattern-match on it, so the representation can change
without rippling through call sites.
Multiple backends can coexist at runtime. Because the backing store is
private, different `Object` instances in the same process can use
different implementations — e.g., a lazy DB-backed object for `input`,
inline small-map objects for SARIF location records, and a regular
sorted map elsewhere — all interoperating through the same opaque
type. This is stronger than the typical Cargo-feature-selected backend
seen in precedent crates.
Iteration is split intentionally. `iter()` makes no ordering promise,
which lets backends that don't keep entries sorted skip any sort work.
`iter_sorted()` returns entries in `Value` order and is what
serialization and `Ord` rely on for deterministic output. Cursor types
add resumable, incremental traversal for the RVM iteration state
without leaking iterator internals.
`Ord` and `PartialOrd` are defined against `iter_sorted()` rather than
derived from the storage. Two `Object`s built on different backends —
or with different insertion histories — compare equal whenever their
sorted entries match, so changing the backend never changes observable
comparison results.
## Precedents
Other crates that hide storage behind a stable API so the implementation
can change without breaking callers:
- **`serde_json::Map`** — opaque newtype allowing cargo-feature based
swap between `BTreeMap` (canonical order) and `IndexMap` (insertion
order).
- **`toml::Table`** — opaque newtype allowing cargo-feature based swap
between `BTreeMap` and `IndexMap`.
- **`simdjson` DOM** — opaque tree that lazily materializes nodes on
access instead of parsing the whole document up front.
## Use cases
- **SARIF small-object pressure** — SARIF reports contain millions of
small objects (location records, rule references, message arguments),
most with 2-5 keys. A small-map-optimized backend (inline storage
for ≤N entries, heap above) eliminates per-object BTreeMap allocation
for the common case.
- **Kubernetes admission policies** — large, deeply-nested resource
objects (Pod specs, CRDs) where policies typically touch a handful
of paths. A lazy-materializing backend (`LazyObjectProvider` over
the incoming JSON) parses only the accessed subtrees.
- **Azure Policy aliases** — ARM exposes the same logical property
under multiple aliases (e.g. paths like
`Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.id`).
An alias-aware backend resolves lookups across canonical and alias
forms without rewriting every policy.
- **Azure Policy case-insensitive compare** — ARM property names are
case-preserving but case-insensitive on lookup (`tags.Environment`
and `tags.environment` resolve identically). A case-insensitive
backend centralizes this once at the storage layer instead of at
every comparison site.
- **External data sources** — `input` or `data` backed by a database
query, CBOR slice, REST endpoint, or other streaming source via a
`LazyObjectProvider`. Entries materialize on demand; the policy
only pays for what it touches.
- **Eval-time temporaries** — objects constructed during evaluation
(comprehensions, intermediate rule results) on a bumpalo arena.
The whole arena drops at query end with zero per-entry free cost.
- **Host-language interop** — Python dicts or JS objects accessed via
FFI callbacks from the embedding application, without copying into
Rust on every binding boundary.
+1 -1
View File
@@ -2,7 +2,7 @@
name = "regorus-mimalloc" name = "regorus-mimalloc"
description = "Vendored mimalloc allocator for regorus" description = "Vendored mimalloc allocator for regorus"
edition = "2021" edition = "2021"
version = "2.2.6" version = "2.2.7"
license = "MIT" license = "MIT"
repository = "https://github.com/microsoft/regorus" repository = "https://github.com/microsoft/regorus"
+1 -1
View File
@@ -319,7 +319,7 @@ pub fn resolve_path(root: &Value, path: &str) -> Value {
match &current { match &current {
Value::Object(map) => { Value::Object(map) => {
let mut next = None; let mut next = None;
for (key, value) in map.iter() { for (key, value) in map.iter_sorted() {
if let Value::String(ref key_str) = *key { if let Value::String(ref key_str) = *key {
if strings::keys::eq(key_str, &segment) { if strings::keys::eq(key_str, &segment) {
next = Some(value.clone()); next = Some(value.clone());
@@ -8,10 +8,10 @@
use crate::ast::{Expr, Ref}; use crate::ast::{Expr, Ref};
use crate::builtins; use crate::builtins;
use crate::lexer::Span; use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value; use crate::value::Value;
use crate::Rc; use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::Result; use anyhow::Result;
@@ -72,7 +72,7 @@ fn fn_intersection(
// Intersection of objects: keep key-value pairs from the first // Intersection of objects: keep key-value pairs from the first
// object only when the key exists in every other object AND // object only when the key exists in every other object AND
// the value is equal across all of them. // the value is equal across all of them.
let mut result: BTreeMap<Value, Value> = first.as_ref().clone(); let mut result: Object = first.as_ref().clone();
for arg in rest { for arg in rest {
let Value::Object(ref other) = *arg else { let Value::Object(ref other) = *arg else {
return Ok(Value::Undefined); return Ok(Value::Undefined);
@@ -114,7 +114,7 @@ fn fn_union(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool)
Value::Object(_) => { Value::Object(_) => {
// Union of objects: recursive merge. Nested objects are merged // Union of objects: recursive merge. Nested objects are merged
// recursively; all other types (including arrays) use last-writer-wins. // recursively; all other types (including arrays) use last-writer-wins.
let mut result = BTreeMap::<Value, Value>::new(); let mut result = Object::new();
for arg in args { for arg in args {
let Value::Object(ref obj) = *arg else { let Value::Object(ref obj) = *arg else {
return Ok(Value::Undefined); return Ok(Value::Undefined);
@@ -264,7 +264,7 @@ fn fn_create_object(
); );
} }
let mut map = BTreeMap::<Value, Value>::new(); let mut map = Object::new();
for pair in args.chunks(2) { for pair in args.chunks(2) {
#[allow(clippy::pattern_type_mismatch)] #[allow(clippy::pattern_type_mismatch)]
@@ -280,9 +280,9 @@ fn fn_create_object(
/// Recursively merge two objects. Nested objects are merged; everything /// Recursively merge two objects. Nested objects are merged; everything
/// else (including arrays) uses the value from `incoming`. /// else (including arrays) uses the value from `incoming`.
fn merge_objects(base: &BTreeMap<Value, Value>, overlay: &BTreeMap<Value, Value>) -> Value { fn merge_objects(base: &Object, overlay: &Object) -> Value {
let mut result = base.clone(); let mut result = base.clone();
for (k, v) in overlay { for (k, v) in overlay.iter() {
#[allow(clippy::needless_borrowed_reference)] #[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) { let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => merge_objects(prev, next), (Some(&Value::Object(ref prev)), &Value::Object(ref next)) => merge_objects(prev, next),
@@ -8,10 +8,10 @@
use crate::ast::{Expr, Ref}; use crate::ast::{Expr, Ref};
use crate::builtins; use crate::builtins;
use crate::lexer::Span; use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value; use crate::value::Value;
use crate::Rc; use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _}; use alloc::string::{String, ToString as _};
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::Result; use anyhow::Result;
@@ -84,8 +84,8 @@ fn fn_items(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool)
return Ok(Value::Undefined); return Ok(Value::Undefined);
}; };
let mut result = Vec::with_capacity(obj.len()); let mut result = Vec::with_capacity(obj.len());
for (k, v) in obj.as_ref() { for (k, v) in obj.iter_sorted() {
let mut entry = BTreeMap::<Value, Value>::new(); let mut entry = Object::new();
entry.insert(Value::from("key"), k.clone()); entry.insert(Value::from("key"), k.clone());
entry.insert(Value::from("value"), v.clone()); entry.insert(Value::from("value"), v.clone());
result.push(Value::Object(Rc::new(entry))); result.push(Value::Object(Rc::new(entry)));
+1 -1
View File
@@ -308,7 +308,7 @@ fn urlquery_encode_object(
{ {
let mut pairs = url.query_pairs_mut(); let mut pairs = url.query_pairs_mut();
for (key, value) in obj.iter() { for (key, value) in obj.iter_sorted() {
let key = ensure_string(name, &params[0], key)?; let key = ensure_string(name, &params[0], key)?;
match value { match value {
Value::String(v) => { Value::String(v) => {
+4 -3
View File
@@ -7,10 +7,11 @@ use crate::ast::{Expr, Ref};
use crate::builtins; use crate::builtins;
use crate::builtins::utils::{enforce_limit, ensure_args_count, ensure_object}; use crate::builtins::utils::{enforce_limit, ensure_args_count, ensure_object};
use crate::lexer::Span; use crate::lexer::Span;
use crate::value::Object;
use crate::value::Value; use crate::value::Value;
use crate::*; use crate::*;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::BTreeSet;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -80,7 +81,7 @@ fn reachable(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) ->
} }
fn visit( fn visit(
graph: &BTreeMap<Value, Value>, graph: &Object,
visited: &mut BTreeSet<Value>, visited: &mut BTreeSet<Value>,
node: &Value, node: &Value,
path: &mut Vec<Value>, path: &mut Vec<Value>,
@@ -211,7 +212,7 @@ fn walk_visit(path: &mut Vec<Value>, value: &Value, paths: &mut Vec<Value>) -> R
} }
} }
Value::Object(obj) => { Value::Object(obj) => {
for (key, value) in obj.iter() { for (key, value) in obj.iter_sorted() {
path.push(key.clone()); path.push(key.clone());
// Guard path stack growth while traversing object entries. // Guard path stack growth while traversing object entries.
enforce_limit()?; enforce_limit()?;
+1 -1
View File
@@ -205,7 +205,7 @@ fn merge_filters(
let vref = match f { let vref = match f {
Value::Object(obj) => { Value::Object(obj) => {
let obj = Rc::make_mut(obj); let obj = Rc::make_mut(obj);
let entry = obj.entry(p.clone()).or_insert_with(Value::new_object); let entry = obj.get_or_insert_with(p.clone(), Value::new_object);
// Guard filter map growth when creating nested objects. // Guard filter map growth when creating nested objects.
enforce_limit()?; enforce_limit()?;
entry entry
+2 -2
View File
@@ -207,7 +207,7 @@ fn to_string(v: &Value, unescape: bool) -> String {
} }
Value::Object(o) => { Value::Object(o) => {
"{".to_owned() "{".to_owned()
+ &o.iter() + &o.iter_sorted()
.map(|(k, v)| to_string(k, true) + ": " + &to_string(v, true)) .map(|(k, v)| to_string(k, true) + ": " + &to_string(v, true))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", ") .join(", ")
@@ -568,7 +568,7 @@ fn replace_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -
let mut s = ensure_string(name, &params[1], &args[1])?; let mut s = ensure_string(name, &params[1], &args[1])?;
let span = params[0].span(); let span = params[0].span();
for item in obj.as_ref().iter() { for item in obj.as_ref().iter_sorted() {
match item { match item {
(Value::String(k), Value::String(v)) => { (Value::String(k), Value::String(v)) => {
s = s.replace(k.as_ref(), v.as_ref()).into(); s = s.replace(k.as_ref(), v.as_ref()).into();
+3 -2
View File
@@ -5,11 +5,12 @@
use crate::ast::{Expr, Ref}; use crate::ast::{Expr, Ref};
use crate::lexer::Span; use crate::lexer::Span;
use crate::number::Number; use crate::number::Number;
use crate::value::Object;
use crate::Rc; use crate::Rc;
use crate::Value; use crate::Value;
use crate::*; use crate::*;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::BTreeSet;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -168,7 +169,7 @@ pub fn ensure_set(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<BTreeSet<Value>>
}) })
} }
pub fn ensure_object(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<BTreeMap<Value, Value>>> { pub fn ensure_object(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<Object>> {
Ok(match v { Ok(match v {
Value::Object(o) => o, Value::Object(o) => o,
_ => { _ => {
@@ -314,7 +314,7 @@ fn order_element_pairs<T: VariableBindingContext>(
if ready { if ready {
let (value_expr, plan, _deps, binds) = remaining.remove(idx); let (value_expr, plan, _deps, binds) = remaining.remove(idx);
scheduled.extend(binds.into_iter()); scheduled.extend(binds);
ordered.push((value_expr, plan)); ordered.push((value_expr, plan));
progress = true; progress = true;
break; break;
+21 -39
View File
@@ -28,7 +28,6 @@ use crate::{Expression, Extension, Location, QueryResult, QueryResults};
use crate::query::traversal::traverse; use crate::query::traversal::traverse;
use crate::Rc; use crate::Rc;
use alloc::collections::btree_map::Entry as BTreeMapEntry;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use core::ops::Bound::*; use core::ops::Bound::*;
@@ -1312,10 +1311,10 @@ impl Interpreter {
*obj = Value::new_object(); *obj = Value::new_object();
} }
obj = obj obj = obj.as_object_mut()?.get_or_insert_with(
.as_object_mut()? Value::String(p.to_string().into()),
.entry(Value::String(p.to_string().into())) Value::new_object,
.or_insert(Value::new_object()); );
} }
*obj = value; *obj = value;
// Mark modified rules as processed. // Mark modified rules as processed.
@@ -1682,8 +1681,7 @@ impl Interpreter {
let set = obj let set = obj
.as_object_mut() .as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))? .map_err(|_| anyhow!(span.error("previous value is not an object")))?
.entry(p) .get_or_insert_with(p, Value::new_set)
.or_insert(Value::new_set())
.as_set_mut() .as_set_mut()
.map_err(|_| anyhow!(span.error("previous value is not a set")))?; .map_err(|_| anyhow!(span.error("previous value is not a set")))?;
set.append(value.as_set_mut()?); set.append(value.as_set_mut()?);
@@ -1691,20 +1689,13 @@ impl Interpreter {
let obj = obj let obj = obj
.as_object_mut() .as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))?; .map_err(|_| anyhow!(span.error("previous value is not an object")))?;
match obj.entry(p) { if value == Value::Undefined {
BTreeMapEntry::Vacant(v) => { // TODO: clean this assumption between Undefined vs Object.
if value != Value::Undefined { obj.get_or_insert_with(p, Value::new_object);
v.insert(value); } else {
} else { let existing = obj.get_or_insert_with(p, || value.clone());
// TODO: clean this assumption between Undefined vs Object. if *existing != value {
v.insert(Value::new_object()); bail!(span.error("complete rules should not produce multiple outputs"))
}
}
BTreeMapEntry::Occupied(o) => {
if o.get() != &value && value != Value::Undefined {
bail!(span
.error("complete rules should not produce multiple outputs"))
}
} }
} }
} }
@@ -1713,8 +1704,7 @@ impl Interpreter {
obj = obj obj = obj
.as_object_mut() .as_object_mut()
.map_err(|_| anyhow!(span.error("previous value is not an object")))? .map_err(|_| anyhow!(span.error("previous value is not an object")))?
.entry(p) .get_or_insert_with(p, Value::new_object);
.or_insert(Value::new_object());
} }
} }
Ok(()) Ok(())
@@ -1822,8 +1812,7 @@ impl Interpreter {
let set = ctx_mut let set = ctx_mut
.rule_value .rule_value
.as_object_mut()? .as_object_mut()?
.entry(Value::from_array(comps)) .get_or_insert_with(Value::from_array(comps), Value::new_set);
.or_insert(Value::new_set());
if output != Value::Undefined { if output != Value::Undefined {
set.as_set_mut()?.insert(output); set.as_set_mut()?.insert(output);
return Ok(true); return Ok(true);
@@ -1832,20 +1821,13 @@ impl Interpreter {
} }
// Non-set rule. // Non-set rule.
match ctx_mut let key = Value::from_array(comps);
.rule_value let obj_mut = ctx_mut.rule_value.as_object_mut()?;
.as_object_mut()? let existing = obj_mut.get_or_insert_with(key, || output.clone());
.entry(Value::from_array(comps)) if *existing != output {
{ bail!(rule_ref
BTreeMapEntry::Vacant(v) => {
v.insert(output);
}
BTreeMapEntry::Occupied(o) if o.get() != &output => bail!(rule_ref
.span() .span()
.error("rules must not produce multiple outputs")), .error("rules must not produce multiple outputs"));
_ => {
// Rule produced same value.
}
} }
return Ok(true); return Ok(true);
@@ -2471,7 +2453,7 @@ impl Interpreter {
} }
Value::Object(map) => { Value::Object(map) => {
s.push('{'); s.push('{');
for (idx, (k, entry_value)) in map.iter().enumerate() { for (idx, (k, entry_value)) in map.iter_sorted().enumerate() {
if idx > 0 { if idx > 0 {
s.push_str(", "); s.push_str(", ");
} }
@@ -213,10 +213,10 @@ pub fn denormalize_with_aliases(
// Phase 4: Attach properties to result. // Phase 4: Attach properties to result.
if !properties.is_empty() { if !properties.is_empty() {
if let Some(Value::Object(existing_rc)) = result.get_mut("properties") { if let Some(Value::Object(existing_rc)) = result.get_mut("properties") {
// Merge directly into the BTreeMap, avoiding full ObjMap round-trip. // Merge directly into the Object, avoiding full ObjMap round-trip.
let existing = Rc::make_mut(existing_rc); let existing = Rc::make_mut(existing_rc);
for (k, v) in properties { for (k, v) in properties {
existing.entry(Value::String(k)).or_insert(v); existing.get_or_insert_with(Value::String(k), || v);
} }
} else { } else {
obj_insert(&mut result, "properties", make_value(properties)); obj_insert(&mut result, "properties", make_value(properties));
@@ -7,6 +7,7 @@ use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use crate::value::Object;
use crate::Value; use crate::Value;
use super::super::obj_map::{make_value, new_map, obj_insert, val_str, ObjMap}; use super::super::obj_map::{make_value, new_map, obj_insert, val_str, ObjMap};
@@ -141,7 +142,7 @@ fn rewrap_nested_array(
/// BTreeMap-native recursion for nested sub-resource array re-wrapping, /// BTreeMap-native recursion for nested sub-resource array re-wrapping,
/// avoiding ObjMap round-trips on each array element. /// avoiding ObjMap round-trips on each array element.
fn rewrap_nested_array_in_btree( fn rewrap_nested_array_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>, btree: &mut Object,
parent_parts: &[&str], parent_parts: &[&str],
array_name: &str, array_name: &str,
envelope_fields: &BTreeSet<String>, envelope_fields: &BTreeSet<String>,
@@ -187,10 +188,7 @@ fn rewrap_nested_array_in_btree(
} }
/// Find a key in a BTreeMap using case-insensitive comparison. /// Find a key in a BTreeMap using case-insensitive comparison.
fn find_key_ci_btree( fn find_key_ci_btree(btree: &Object, key: &str) -> Option<Value> {
btree: &alloc::collections::BTreeMap<Value, Value>,
key: &str,
) -> Option<Value> {
btree btree
.keys() .keys()
.find(|k| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(key))) .find(|k| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(key)))
@@ -6,11 +6,12 @@
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use crate::value::Object;
use crate::Value; use crate::Value;
use super::super::obj_map::{ use super::super::obj_map::{
obj_get, obj_get_mut, obj_insert, set_nested_in_btree, set_nested_lowercased, obj_get, obj_get_mut, obj_insert, set_nested, set_nested_lowercased, set_nested_verbatim,
set_nested_verbatim, ObjMap, ObjMap,
}; };
use super::super::types::PrecomputedRemap; use super::super::types::PrecomputedRemap;
@@ -118,7 +119,7 @@ fn apply_remap_at_depth(
/// BTreeMap-native recursion for element-level remap, avoiding ObjMap /// BTreeMap-native recursion for element-level remap, avoiding ObjMap
/// round-trips on each array element. /// round-trips on each array element.
fn remap_at_depth_in_btree( fn remap_at_depth_in_btree(
btree: &mut alloc::collections::BTreeMap<Value, Value>, btree: &mut Object,
array_chain: &[Vec<String>], array_chain: &[Vec<String>],
depth: usize, depth: usize,
source_field: &str, source_field: &str,
@@ -177,12 +178,7 @@ fn remap_at_depth_in_btree(
} }
/// Remap a value between dotted paths directly in a BTreeMap. /// Remap a value between dotted paths directly in a BTreeMap.
fn remap_deep_field_in_btree( fn remap_deep_field_in_btree(btree: &mut Object, source: &str, target: &str, lowercase: bool) {
btree: &mut alloc::collections::BTreeMap<Value, Value>,
source: &str,
target: &str,
lowercase: bool,
) {
let val = match read_dotted_path_btree(btree, source) { let val = match read_dotted_path_btree(btree, source) {
Some(v) => v, Some(v) => v,
None => return, None => return,
@@ -198,14 +194,11 @@ fn remap_deep_field_in_btree(
} }
return; return;
} }
set_nested_in_btree(btree, &segments, val, lowercase); set_nested(btree, &segments, val, lowercase);
} }
/// Read a value at a dotted path from a BTreeMap. /// Read a value at a dotted path from a BTreeMap.
fn read_dotted_path_btree( fn read_dotted_path_btree(btree: &Object, path: &str) -> Option<Value> {
btree: &alloc::collections::BTreeMap<Value, Value>,
path: &str,
) -> Option<Value> {
let segments: Vec<&str> = path.split('.').collect(); let segments: Vec<&str> = path.split('.').collect();
let first = segments.first()?; let first = segments.first()?;
let mut cur: &Value = btree.get(&Value::from(*first))?; let mut cur: &Value = btree.get(&Value::from(*first))?;
@@ -13,6 +13,7 @@ mod flatten;
// Re-export items used by the denormalizer. // Re-export items used by the denormalizer.
pub(crate) use element_remap::{apply_element_remap, ElementRemap}; pub(crate) use element_remap::{apply_element_remap, ElementRemap};
use crate::value::Object;
use crate::Value; use crate::Value;
use super::obj_map::{ use super::obj_map::{
@@ -109,7 +110,7 @@ pub fn normalize_with_aliases(
/// Merge `properties` fields into the result map, skipping keys that already /// Merge `properties` fields into the result map, skipping keys that already
/// exist. /// exist.
fn merge_properties( fn merge_properties(
obj: &alloc::collections::BTreeMap<Value, Value>, obj: &Object,
result: &mut ObjMap, result: &mut ObjMap,
sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>, sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>,
) { ) {
+33 -51
View File
@@ -4,7 +4,7 @@
//! Lightweight string-keyed map used during normalization/denormalization. //! Lightweight string-keyed map used during normalization/denormalization.
//! //!
//! Internally uses `hashbrown::HashMap<Rc<str>, Value>` for O(1) lookups, //! Internally uses `hashbrown::HashMap<Rc<str>, Value>` for O(1) lookups,
//! then converts to `Value::Object` (a `BTreeMap<Value, Value>`) only at //! then converts to `Value::Object` (an `Object`) only at
//! the output boundary via [`make_value`]. //! the output boundary via [`make_value`].
use alloc::string::String; use alloc::string::String;
@@ -12,6 +12,7 @@ use alloc::vec::Vec;
use hashbrown::HashMap; use hashbrown::HashMap;
use crate::value::Object;
use crate::Rc; use crate::Rc;
use crate::Value; use crate::Value;
@@ -81,14 +82,13 @@ pub fn obj_remove(map: &mut ObjMap, key: &str) -> Option<Value> {
/// Convert an [`ObjMap`] into a [`Value::Object`]. /// Convert an [`ObjMap`] into a [`Value::Object`].
/// ///
/// Keys are converted from `Rc<str>` to `Value::String` and inserted into /// Keys are converted from `Rc<str>` to `Value::String` and inserted into
/// a `BTreeMap` to match the `Value::Object` representation. /// an `Object` to match the `Value::Object` representation.
pub fn make_value(map: ObjMap) -> Value { pub fn make_value(map: ObjMap) -> Value {
use alloc::collections::BTreeMap; let obj: Object = map
let mut btree = BTreeMap::new(); .into_iter()
for (k, v) in map { .map(|(k, v)| (Value::String(k), v))
btree.insert(Value::String(k), v); .collect();
} Value::Object(Rc::new(obj))
Value::Object(Rc::new(btree))
} }
/// Convert a `Vec<Value>` into a `Value::Array`. /// Convert a `Vec<Value>` into a `Value::Array`.
@@ -115,14 +115,14 @@ pub fn extract_type_field(resource: &Value) -> Option<&str> {
}) })
} }
/// Convert a `Value::Object` (BTreeMap<Value, Value>) into an [`ObjMap`]. /// Convert a `Value::Object` (Object) into an [`ObjMap`].
/// ///
/// Non-string keys are silently skipped. /// Non-string keys are silently skipped.
#[allow(dead_code)] #[allow(dead_code)]
pub fn value_to_obj_map(value: &Value) -> Option<ObjMap> { pub fn value_to_obj_map(value: &Value) -> Option<ObjMap> {
let btree = value.as_object().ok()?; let obj = value.as_object().ok()?;
let mut map = ObjMap::with_capacity(btree.len()); let mut map = ObjMap::with_capacity(obj.len());
for (k, v) in btree.iter() { for (k, v) in obj.iter() {
if let Value::String(s) = k { if let Value::String(s) = k {
map.insert(Rc::clone(s), v.clone()); map.insert(Rc::clone(s), v.clone());
} }
@@ -194,7 +194,7 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
// Descend directly into the BTreeMap, avoiding ObjMap round-trip. // Descend directly into the BTreeMap, avoiding ObjMap round-trip.
if let Some(Value::Object(inner_rc)) = obj.get_mut(&*seg) { if let Some(Value::Object(inner_rc)) = obj.get_mut(&*seg) {
let inner_btree = Rc::make_mut(inner_rc); let inner_btree = Rc::make_mut(inner_rc);
set_nested_in_btree( set_nested(
inner_btree, inner_btree,
segments.get(1..).unwrap_or_default(), segments.get(1..).unwrap_or_default(),
value, value,
@@ -203,17 +203,12 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
} }
} }
/// Set a value at a path directly in a `BTreeMap<Value, Value>`, creating /// Set a value at a path directly in an `Object`, creating
/// intermediate `Value::Object` nodes as needed. /// intermediate `Value::Object` nodes as needed.
/// ///
/// This avoids the `btree_to_obj_map` / `obj_map_to_btree` round-trip that /// This avoids the `btree_to_obj_map` / `obj_map_to_btree` round-trip that
/// would clone every sibling entry at each nesting level. /// would clone every sibling entry at each nesting level.
pub fn set_nested_in_btree( pub fn set_nested(obj: &mut Object, segments: &[&str], value: Value, lowercase: bool) {
btree: &mut alloc::collections::BTreeMap<Value, Value>,
segments: &[&str],
value: Value,
lowercase: bool,
) {
let Some(&first) = segments.first() else { let Some(&first) = segments.first() else {
return; return;
}; };
@@ -226,18 +221,18 @@ pub fn set_nested_in_btree(
let key_val = Value::String(Rc::clone(&key_rc)); let key_val = Value::String(Rc::clone(&key_rc));
if segments.len() == 1 { if segments.len() == 1 {
btree.insert(key_val, value); obj.insert(key_val, value);
return; return;
} }
// Ensure an intermediate object exists. // Ensure an intermediate object exists.
if !btree.contains_key(&key_val) { if !obj.contains_key(&key_val) {
btree.insert(key_val.clone(), make_value(new_map())); obj.insert(key_val.clone(), make_value(new_map()));
} }
if let Some(Value::Object(inner_rc)) = btree.get_mut(&key_val) { if let Some(Value::Object(inner_rc)) = obj.get_mut(&key_val) {
let inner = Rc::make_mut(inner_rc); let inner = Rc::make_mut(inner_rc);
set_nested_in_btree( set_nested(
inner, inner,
segments.get(1..).unwrap_or_default(), segments.get(1..).unwrap_or_default(),
value, value,
@@ -352,20 +347,15 @@ fn remove_field_at_depth(obj: &mut ObjMap, array_chain: &[Vec<String>], depth: u
for elem in inner.iter_mut() { for elem in inner.iter_mut() {
if let Value::Object(obj_rc) = elem { if let Value::Object(obj_rc) = elem {
let inner_btree = Rc::make_mut(obj_rc); let inner_btree = Rc::make_mut(obj_rc);
remove_field_at_depth_in_btree( remove_field_at_depth_obj(inner_btree, array_chain, depth.saturating_add(1), field);
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
} }
} }
} }
} }
/// BTreeMap-native recursion for element-level field removal. /// Object-native recursion for element-level field removal.
fn remove_field_at_depth_in_btree( fn remove_field_at_depth_obj(
btree: &mut alloc::collections::BTreeMap<Value, Value>, obj: &mut Object,
array_chain: &[Vec<String>], array_chain: &[Vec<String>],
depth: usize, depth: usize,
field: &str, field: &str,
@@ -374,10 +364,10 @@ fn remove_field_at_depth_in_btree(
let segments: Vec<&str> = field.split('.').collect(); let segments: Vec<&str> = field.split('.').collect();
if segments.len() == 1 { if segments.len() == 1 {
if let Some(&seg) = segments.first() { if let Some(&seg) = segments.first() {
btree.remove(&Value::from(seg)); obj.remove(&Value::from(seg));
} }
} else if segments.len() > 1 { } else if segments.len() > 1 {
remove_at_dotted_path_in_btree(btree, &segments); remove_at_dotted_path_obj(obj, &segments);
} }
return; return;
}; };
@@ -389,12 +379,12 @@ fn remove_field_at_depth_in_btree(
let key_val = Value::from(first); let key_val = Value::from(first);
let arr_val = if nav.len() == 1 { let arr_val = if nav.len() == 1 {
match btree.get_mut(&key_val) { match obj.get_mut(&key_val) {
Some(v) => v, Some(v) => v,
None => return, None => return,
} }
} else { } else {
let mut cur: &mut Value = match btree.get_mut(&key_val) { let mut cur: &mut Value = match obj.get_mut(&key_val) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };
@@ -415,27 +405,19 @@ fn remove_field_at_depth_in_btree(
for elem in inner.iter_mut() { for elem in inner.iter_mut() {
if let Value::Object(obj_rc) = elem { if let Value::Object(obj_rc) = elem {
let inner_btree = Rc::make_mut(obj_rc); let inner_btree = Rc::make_mut(obj_rc);
remove_field_at_depth_in_btree( remove_field_at_depth_obj(inner_btree, array_chain, depth.saturating_add(1), field);
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
} }
} }
} }
} }
/// Remove the leaf segment at a dotted path directly in a BTreeMap. /// Remove the leaf segment at a dotted path directly in an Object.
fn remove_at_dotted_path_in_btree( fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) {
btree: &mut alloc::collections::BTreeMap<Value, Value>,
segments: &[&str],
) {
let Some((&leaf, parent_segs)) = segments.split_last() else { let Some((&leaf, parent_segs)) = segments.split_last() else {
return; return;
}; };
if parent_segs.is_empty() { if parent_segs.is_empty() {
btree.remove(&Value::from(leaf)); obj.remove(&Value::from(leaf));
return; return;
} }
@@ -443,7 +425,7 @@ fn remove_at_dotted_path_in_btree(
return; return;
}; };
let first_key = Value::from(first); let first_key = Value::from(first);
let parent_val = match btree.get_mut(&first_key) { let parent_val = match obj.get_mut(&first_key) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };
@@ -11,7 +11,7 @@
//! to fetch a related resource and an optional `existenceCondition` evaluated //! to fetch a related resource and an optional `existenceCondition` evaluated
//! inline. //! inline.
use alloc::collections::BTreeMap; use crate::value::Object;
use alloc::format; use alloc::format;
use alloc::string::ToString as _; use alloc::string::ToString as _;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -814,7 +814,7 @@ pub(super) fn build_object_from_keys(
span: &crate::lexer::Span, span: &crate::lexer::Span,
) -> Result<u8> { ) -> Result<u8> {
// Build template: object with all keys set to Undefined. // Build template: object with all keys set to Undefined.
let mut template = BTreeMap::new(); let mut template = Object::new();
for &(key_idx, _) in &keys { for &(key_idx, _) in &keys {
// key_idx was returned by `add_literal_u16` in the calling code, // key_idx was returned by `add_literal_u16` in the calling code,
// so it is always in bounds. We use `.get()` + `?` instead of // so it is always in bounds. We use `.get()` + `?` instead of
@@ -272,7 +272,7 @@ impl Compiler {
fn insert_string_set_annotation( fn insert_string_set_annotation(
annot: &mut alloc::collections::BTreeMap<String, Value>, annot: &mut alloc::collections::BTreeMap<String, Value>,
key: &str, key: &str,
observed: &BTreeSet<String>, observed: &alloc::collections::BTreeSet<String>,
) { ) {
if !observed.is_empty() { if !observed.is_empty() {
let set: BTreeSet<Value> = observed let set: BTreeSet<Value> = observed
@@ -264,18 +264,17 @@ impl<'source> Parser<'source> {
"metadata" => { "metadata" => {
*metadata = Some(self.parse_json_value()?); *metadata = Some(self.parse_json_value()?);
} }
"parameters" => { "parameters" if self.token_text() == "{" => {
// Parameters must be a JSON object; if not, push to extra. // Parameters must be a JSON object; if not, push to extra.
if self.token_text() == "{" { *parameters = self.parse_parameter_definitions()?;
*parameters = self.parse_parameter_definitions()?; }
} else { "parameters" => {
let value = self.parse_json_value()?; let value = self.parse_json_value()?;
extra.push(ObjectEntry { extra.push(ObjectEntry {
key_span, key_span,
key: key.into(), key: key.into(),
value, value,
}); });
}
} }
"policyrule" => { "policyrule" => {
// Parse the policyRule directly from the token stream! // Parse the policyRule directly from the token stream!
@@ -11,8 +11,9 @@ use crate::ast::{Expr, ExprRef};
use crate::lexer::Span; use crate::lexer::Span;
use crate::rvm::instructions::{ArrayCreateParams, ObjectCreateParams, SetCreateParams}; use crate::rvm::instructions::{ArrayCreateParams, ObjectCreateParams, SetCreateParams};
use crate::rvm::Instruction; use crate::rvm::Instruction;
use crate::value::Object;
use crate::{Rc, Value}; use crate::{Rc, Value};
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::BTreeSet;
use alloc::vec::Vec; use alloc::vec::Vec;
/// Try to evaluate an expression as a compile-time constant. /// Try to evaluate an expression as a compile-time constant.
@@ -43,7 +44,7 @@ pub(in crate::languages::rego::compiler) fn try_eval_const(expr: &Expr) -> Optio
Expr::Object { fields, .. } => fields Expr::Object { fields, .. } => fields
.iter() .iter()
.map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?))) .map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?)))
.collect::<Option<BTreeMap<_, _>>>() .collect::<Option<Object>>()
.map(|m| Value::Object(Rc::new(m))), .map(|m| Value::Object(Rc::new(m))),
_ => None, _ => None,
} }
@@ -117,7 +118,7 @@ impl<'a> Compiler<'a> {
fields: &[(crate::lexer::Span, ExprRef, ExprRef)], fields: &[(crate::lexer::Span, ExprRef, ExprRef)],
span: &Span, span: &Span,
) -> Result<Register> { ) -> Result<Register> {
let all_const: Option<BTreeMap<_, _>> = fields let all_const: Option<Object> = fields
.iter() .iter()
.map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?))) .map(|(_, k, v)| Some((try_eval_const(k.as_ref())?, try_eval_const(v.as_ref())?)))
.collect(); .collect();
@@ -166,7 +167,7 @@ impl<'a> Compiler<'a> {
let mut template_keys = literal_keys.clone(); let mut template_keys = literal_keys.clone();
template_keys.sort(); template_keys.sort();
let mut template_obj = BTreeMap::new(); let mut template_obj = Object::new();
for key in &template_keys { for key in &template_keys {
template_obj.insert(key.clone(), Value::Undefined); template_obj.insert(key.clone(), Value::Undefined);
} }
+1 -1
View File
@@ -155,7 +155,7 @@ pub mod target;
#[cfg(any(test, all(feature = "yaml", feature = "std")))] #[cfg(any(test, all(feature = "yaml", feature = "std")))]
pub mod test_utils; pub mod test_utils;
pub mod utils; pub mod utils;
mod value; pub mod value;
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
pub use { pub use {
+6 -7
View File
@@ -11,9 +11,9 @@
//! values are converted through [`MetadataValue`] — a postcard/bincode-safe //! values are converted through [`MetadataValue`] — a postcard/bincode-safe
//! enum that avoids `deserialize_any`. //! enum that avoids `deserialize_any`.
use crate::value::Object;
use crate::Rc; use crate::Rc;
use alloc::collections::BTreeMap; use alloc::collections::{BTreeMap, BTreeSet};
use alloc::collections::BTreeSet;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -52,7 +52,7 @@ impl ProgramMetadata {
pub fn to_value(&self) -> crate::value::Value { pub fn to_value(&self) -> crate::value::Value {
use crate::value::Value; use crate::value::Value;
let mut obj = BTreeMap::new(); let mut obj = Object::new();
obj.insert( obj.insert(
Value::String("compiler_version".into()), Value::String("compiler_version".into()),
Value::String(self.compiler_version.as_str().into()), Value::String(self.compiler_version.as_str().into()),
@@ -75,7 +75,7 @@ impl ProgramMetadata {
); );
if !self.annotations.is_empty() { if !self.annotations.is_empty() {
let mut annotations_obj = BTreeMap::new(); let mut annotations_obj = Object::new();
for (k, v) in &self.annotations { for (k, v) in &self.annotations {
annotations_obj.insert(Value::String(k.as_str().into()), v.clone()); annotations_obj.insert(Value::String(k.as_str().into()), v.clone());
} }
@@ -198,7 +198,7 @@ impl MetadataValue {
match *self { match *self {
MetadataValue::String(ref s) => Value::String(s.as_str().into()), MetadataValue::String(ref s) => Value::String(s.as_str().into()),
MetadataValue::StringSet(ref set) => { MetadataValue::StringSet(ref set) => {
let mut bset = alloc::collections::BTreeSet::new(); let mut bset = BTreeSet::new();
for s in set { for s in set {
bset.insert(Value::String(s.as_str().into())); bset.insert(Value::String(s.as_str().into()));
} }
@@ -211,7 +211,7 @@ impl MetadataValue {
Value::Array(Rc::new(values)) Value::Array(Rc::new(values))
} }
MetadataValue::Map(ref map) => { MetadataValue::Map(ref map) => {
let mut obj = BTreeMap::new(); let mut obj = Object::new();
for (k, v) in map { for (k, v) in map {
obj.insert(Value::String(k.as_str().into()), v.to_value()); obj.insert(Value::String(k.as_str().into()), v.to_value());
} }
@@ -257,7 +257,6 @@ mod metadata_serde {
mod tests { mod tests {
use super::*; use super::*;
use crate::value::Value; use crate::value::Value;
use alloc::collections::BTreeSet;
/// Round-trip: Value → MetadataValue → Value must be equivalent for /// Round-trip: Value → MetadataValue → Value must be equivalent for
/// all lossless variants (strings, bools, integers, arrays, objects). /// all lossless variants (strings, bools, integers, arrays, objects).
+6 -5
View File
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT License. // Licensed under the MIT License.
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::BTreeSet;
use alloc::format; use alloc::format;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -11,6 +11,7 @@ use serde::ser::{SerializeSeq as _, SerializeTuple as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::number::Number; use crate::number::Number;
use crate::value::Object;
use crate::value::Value; use crate::value::Value;
const VARIANT_NULL: u32 = 0; const VARIANT_NULL: u32 = 0;
@@ -132,7 +133,7 @@ impl<'a> Serialize for BinarySetRef<'a> {
} }
} }
struct BinaryObjectRef<'a>(&'a BTreeMap<Value, Value>); struct BinaryObjectRef<'a>(&'a Object);
impl<'a> Serialize for BinaryObjectRef<'a> { impl<'a> Serialize for BinaryObjectRef<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -140,7 +141,7 @@ impl<'a> Serialize for BinaryObjectRef<'a> {
S: serde::Serializer, S: serde::Serializer,
{ {
let mut seq = serializer.serialize_seq(Some(self.0.len()))?; let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for (key, value) in self.0.iter() { for (key, value) in self.0.iter_sorted() {
seq.serialize_element(&BinaryEntryRef(key, value))?; seq.serialize_element(&BinaryEntryRef(key, value))?;
} }
seq.end() seq.end()
@@ -261,11 +262,11 @@ impl<'de> Visitor<'de> for BinaryValueVisitor {
} }
(BinaryVariant::Object, variant) => { (BinaryVariant::Object, variant) => {
let entries: Vec<(BinaryValue, BinaryValue)> = variant.newtype_variant()?; let entries: Vec<(BinaryValue, BinaryValue)> = variant.newtype_variant()?;
let mut map = BTreeMap::new(); let mut map = Object::new();
for (key, value) in entries { for (key, value) in entries {
map.insert(key.into_value(), value.into_value()); map.insert(key.into_value(), value.into_value());
} }
Ok(BinaryValue(Value::from(map))) Ok(BinaryValue(Value::Object(crate::Rc::new(map))))
} }
(BinaryVariant::Undefined, variant) => { (BinaryVariant::Undefined, variant) => {
variant.unit_variant()?; variant.unit_variant()?;
+3 -4
View File
@@ -6,8 +6,6 @@
// Disable both to keep patterns consistent within this file. // Disable both to keep patterns consistent within this file.
#![allow(clippy::pattern_type_mismatch, clippy::needless_borrowed_reference)] #![allow(clippy::pattern_type_mismatch, clippy::needless_borrowed_reference)]
use alloc::collections::BTreeSet;
use crate::number::Number; use crate::number::Number;
use crate::value::Value; use crate::value::Value;
@@ -32,8 +30,9 @@ impl RegoVM {
match (a, b) { match (a, b) {
(&Value::Number(ref x), &Value::Number(ref y)) => Ok(Value::from(x.sub(y)?)), (&Value::Number(ref x), &Value::Number(ref y)) => Ok(Value::from(x.sub(y)?)),
(&Value::Set(ref left), &Value::Set(ref right)) => { (&Value::Set(ref left), &Value::Set(ref right)) => {
let diff: BTreeSet<Value> = left.difference(right).cloned().collect(); let diff: alloc::collections::BTreeSet<Value> =
Ok(Value::from_set(diff)) left.difference(right).cloned().collect();
Ok(Value::from(diff))
} }
_ => Err(VmError::InvalidSubtraction { _ => Err(VmError::InvalidSubtraction {
left: a.clone(), left: a.clone(),
+86 -103
View File
@@ -2,9 +2,9 @@
// Licensed under the MIT License. // Licensed under the MIT License.
use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode}; use crate::rvm::instructions::{ComprehensionBeginParams, ComprehensionMode};
use crate::value::Object;
use crate::value::Value; use crate::value::Value;
use crate::Rc; use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::format; use alloc::format;
use alloc::sync::Arc; use alloc::sync::Arc;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -34,12 +34,12 @@ impl RegoVM {
let initial_result = match params.mode { let initial_result = match params.mode {
ComprehensionMode::Set => Value::new_set(), ComprehensionMode::Set => Value::new_set(),
ComprehensionMode::Array => Value::new_array(), ComprehensionMode::Array => Value::new_array(),
ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())), ComprehensionMode::Object => Value::Object(Rc::new(Object::new())),
}; };
self.set_register(params.result_reg, initial_result.clone())?; self.set_register(params.result_reg, initial_result.clone())?;
let auto_iterate = params.collection_reg != params.result_reg; let auto_iterate = params.collection_reg != params.result_reg;
let iteration_state = if auto_iterate { let mut iteration_state = if auto_iterate {
let source_value = self.get_register(params.collection_reg)?.clone(); let source_value = self.get_register(params.collection_reg)?.clone();
match source_value { match source_value {
Value::Array(items) => { Value::Array(items) => {
@@ -53,11 +53,9 @@ impl RegoVM {
if obj.is_empty() { if obj.is_empty() {
None None
} else { } else {
Some(IterationState::Object { // O(1) cursor over shared Rc<Object>.
obj, let cursor = obj.cursor();
current_key: None, Some(IterationState::Object { obj, cursor })
first_iteration: true,
})
} }
} }
Value::Set(set) => { Value::Set(set) => {
@@ -79,7 +77,7 @@ impl RegoVM {
None None
}; };
let has_iteration = if let Some(state) = iteration_state.as_ref() { let has_iteration = if let Some(state) = iteration_state.as_mut() {
self.setup_next_iteration(state, params.key_reg, params.value_reg)? self.setup_next_iteration(state, params.key_reg, params.value_reg)?
} else { } else {
false false
@@ -123,12 +121,12 @@ impl RegoVM {
let initial_result = match params.mode { let initial_result = match params.mode {
ComprehensionMode::Set => Value::new_set(), ComprehensionMode::Set => Value::new_set(),
ComprehensionMode::Array => Value::new_array(), ComprehensionMode::Array => Value::new_array(),
ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::new())), ComprehensionMode::Object => Value::Object(Rc::new(Object::new())),
}; };
self.set_register(params.result_reg, initial_result.clone())?; self.set_register(params.result_reg, initial_result.clone())?;
let auto_iterate = params.collection_reg != params.result_reg; let auto_iterate = params.collection_reg != params.result_reg;
let iteration_state = if auto_iterate { let mut iteration_state = if auto_iterate {
let source_value = self.get_register(params.collection_reg)?.clone(); let source_value = self.get_register(params.collection_reg)?.clone();
match source_value { match source_value {
Value::Array(items) => { Value::Array(items) => {
@@ -142,11 +140,8 @@ impl RegoVM {
if obj.is_empty() { if obj.is_empty() {
None None
} else { } else {
Some(IterationState::Object { let cursor = obj.cursor();
obj, Some(IterationState::Object { obj, cursor })
current_key: None,
first_iteration: true,
})
} }
} }
Value::Set(set) => { Value::Set(set) => {
@@ -168,7 +163,7 @@ impl RegoVM {
None None
}; };
let has_iteration = if let Some(state) = iteration_state.as_ref() { let has_iteration = if let Some(state) = iteration_state.as_mut() {
self.setup_next_iteration(state, params.key_reg, params.value_reg)? self.setup_next_iteration(state, params.key_reg, params.value_reg)?
} else { } else {
false false
@@ -255,6 +250,21 @@ impl RegoVM {
}; };
let result_reg = comprehension_context.result_reg; let result_reg = comprehension_context.result_reg;
// Snapshot the iteration value register BEFORE taking the result
// register: if the comprehension compiler ever allocates
// `result_reg == context.value_reg`, the writeback at the bottom
// of this function would clobber the value register, and a
// post-writeback read here would feed the wrong value into
// `IterationState::Set::current_item`. Only Set needs the snapshot
// (Object uses a self-advancing cursor; Array advances by index).
let set_resume_snapshot = if matches!(
comprehension_context.iteration_state,
Some(IterationState::Set { .. })
) {
Some(self.get_register(comprehension_context.value_reg)?.clone())
} else {
None
};
// Take ownership of the result register so Rc refcount stays at 1, // Take ownership of the result register so Rc refcount stays at 1,
// allowing Rc::make_mut to mutate in-place instead of deep-cloning. // allowing Rc::make_mut to mutate in-place instead of deep-cloning.
let mut current_result = self.take_register(result_reg)?; let mut current_result = self.take_register(result_reg)?;
@@ -292,29 +302,16 @@ impl RegoVM {
self.set_register(result_reg, current_result)?; self.set_register(result_reg, current_result)?;
if let Some(iter_state) = comprehension_context.iteration_state.as_mut() { if let Some(iter_state) = comprehension_context.iteration_state.as_mut() {
match *iter_state { // Set's `Bound::Excluded(current_item)` resume scheme needs the
IterationState::Object { // pre-mutation snapshot taken at the top of this function.
ref mut current_key, // Object uses a self-advancing cursor and needs no snapshot.
.. if let IterationState::Set {
} => { ref mut current_item,
let tracked_key = ..
if comprehension_context.key_reg != comprehension_context.value_reg { } = *iter_state
self.get_register(comprehension_context.key_reg)?.clone() {
} else { *current_item = set_resume_snapshot;
self.get_register(comprehension_context.value_reg)?.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item =
Some(self.get_register(comprehension_context.value_reg)?.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
} }
iter_state.advance(); iter_state.advance();
let has_next = self.setup_next_iteration( let has_next = self.setup_next_iteration(
iter_state, iter_state,
@@ -359,8 +356,7 @@ impl RegoVM {
result_reg_idx, result_reg_idx,
key_reg_idx, key_reg_idx,
value_reg_idx, value_reg_idx,
iteration_key, iter_is_set,
iteration_value,
) = { ) = {
let frame = let frame =
self.execution_stack self.execution_stack
@@ -382,8 +378,8 @@ impl RegoVM {
let result_reg_idx = context.result_reg; let result_reg_idx = context.result_reg;
let mode = context.mode.clone(); let mode = context.mode.clone();
let iteration_key = self.get_register(context.key_reg)?.clone(); let iter_is_set =
let iteration_value = self.get_register(context.value_reg)?.clone(); matches!(context.iteration_state, Some(IterationState::Set { .. }));
( (
value_to_add, value_to_add,
@@ -392,8 +388,7 @@ impl RegoVM {
result_reg_idx, result_reg_idx,
context.key_reg, context.key_reg,
context.value_reg, context.value_reg,
iteration_key, iter_is_set,
iteration_value,
) )
} else { } else {
return Err(VmError::InvalidIteration { return Err(VmError::InvalidIteration {
@@ -403,6 +398,18 @@ impl RegoVM {
} }
}; };
// Snapshot the iteration value register BEFORE the result writeback:
// if the compiler ever allocates `result_reg == value_reg_idx`, a
// post-writeback read would feed the result accumulator into
// `IterationState::Set::current_item`, breaking the next iteration.
// Only Set needs this (Object cursor self-advances; Array advances
// by index).
let set_resume_snapshot = if iter_is_set {
Some(self.get_register(value_reg_idx)?.clone())
} else {
None
};
// Take ownership of the result register so Rc refcount stays at 1, // Take ownership of the result register so Rc refcount stays at 1,
// allowing Rc::make_mut to mutate in-place instead of deep-cloning. // allowing Rc::make_mut to mutate in-place instead of deep-cloning.
let mut current_result = self.take_register(result_reg_idx)?; let mut current_result = self.take_register(result_reg_idx)?;
@@ -450,27 +457,13 @@ impl RegoVM {
} = &mut frame.kind } = &mut frame.kind
{ {
if let Some(iter_state) = context.iteration_state.as_mut() { if let Some(iter_state) = context.iteration_state.as_mut() {
match *iter_state { if let IterationState::Set {
IterationState::Object { ref mut current_item,
ref mut current_key, ..
.. } = *iter_state
} => { {
let tracked_key = if context.key_reg != context.value_reg { *current_item = set_resume_snapshot;
iteration_key.clone()
} else {
iteration_value.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item = Some(iteration_value.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
} }
iter_state.advance(); iter_state.advance();
} }
@@ -487,8 +480,21 @@ impl RegoVM {
} }
}; };
if let Some(state) = iteration_state_snapshot.as_ref() { if let Some(mut state) = iteration_state_snapshot {
let has_next = self.setup_next_iteration(state, key_reg_idx, value_reg_idx)?; let has_next = self.setup_next_iteration(&mut state, key_reg_idx, value_reg_idx)?;
// `setup_next_iteration` advances Object's internal cursor; the
// owning frame holds the iteration_state, so we must write the
// updated state back. (The Array/Set variants are also unchanged
// by copy, so the writeback is uniform.)
if let Some(frame) = self.execution_stack.get_mut(comprehension_index) {
if let FrameKind::Comprehension {
ref mut context, ..
} = frame.kind
{
context.iteration_state = Some(state);
}
}
if has_next { if has_next {
if let Some(frame) = self.execution_stack.get_mut(comprehension_index) { if let Some(frame) = self.execution_stack.get_mut(comprehension_index) {
@@ -528,7 +534,6 @@ impl RegoVM {
Ok(false) Ok(false)
} }
} }
pub(super) fn handle_comprehension_condition_failure_suspendable(&mut self) -> Result<bool> { pub(super) fn handle_comprehension_condition_failure_suspendable(&mut self) -> Result<bool> {
if let Some(mut frame) = self.execution_stack.pop() { if let Some(mut frame) = self.execution_stack.pop() {
let handled = if let &mut FrameKind::Comprehension { let handled = if let &mut FrameKind::Comprehension {
@@ -554,11 +559,16 @@ impl RegoVM {
context: &mut ComprehensionContext, context: &mut ComprehensionContext,
) -> Result<()> { ) -> Result<()> {
if let Some(iter_state) = context.iteration_state.as_mut() { if let Some(iter_state) = context.iteration_state.as_mut() {
self.capture_comprehension_iteration_position( // Snapshot the current value into Set's `current_item` so the
iter_state, // next iteration can resume from `Bound::Excluded(current)`.
context.key_reg, // Object uses a self-advancing cursor and needs no snapshot here.
context.value_reg, if let IterationState::Set {
)?; ref mut current_item,
..
} = *iter_state
{
*current_item = Some(self.get_register(context.value_reg)?.clone());
}
iter_state.advance(); iter_state.advance();
let has_next = let has_next =
self.setup_next_iteration(iter_state, context.key_reg, context.value_reg)?; self.setup_next_iteration(iter_state, context.key_reg, context.value_reg)?;
@@ -575,37 +585,10 @@ impl RegoVM {
Ok(()) Ok(())
} }
fn capture_comprehension_iteration_position(
&mut self,
iter_state: &mut IterationState,
key_reg: u8,
value_reg: u8,
) -> Result<()> {
match *iter_state {
IterationState::Object {
ref mut current_key,
..
} => {
let tracked_key = if key_reg != value_reg {
self.get_register(key_reg)?.clone()
} else {
self.get_register(value_reg)?.clone()
};
*current_key = Some(tracked_key);
}
IterationState::Set {
ref mut current_item,
..
} => {
*current_item = Some(self.get_register(value_reg)?.clone());
}
IterationState::Array { .. } | IterationState::Single { .. } => {}
}
Ok(())
}
fn execute_comprehension_end_run_to_completion(&mut self) -> Result<()> { fn execute_comprehension_end_run_to_completion(&mut self) -> Result<()> {
// `ComprehensionEnd` is reached from a loaded program; an empty stack
// here means malformed user-supplied bytecode, which must still surface
// as a typed error rather than a panic — including in debug builds.
self.comprehension_stack.pop().map_or_else( self.comprehension_stack.pop().map_or_else(
|| { || {
Err(VmError::InvalidIteration { Err(VmError::InvalidIteration {
+103 -10
View File
@@ -3,8 +3,9 @@
use crate::rvm::instructions::{ComprehensionMode, LoopMode}; use crate::rvm::instructions::{ComprehensionMode, LoopMode};
use crate::value::Value; use crate::value::Value;
use crate::value::{Object, ObjectCursor};
use crate::Rc; use crate::Rc;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::BTreeSet;
use alloc::vec::Vec; use alloc::vec::Vec;
/// Loop execution context for managing iteration state /// Loop execution context for managing iteration state
@@ -24,7 +25,18 @@ pub struct LoopContext {
pub current_iteration_failed: bool, // Track if current iteration had condition failures pub current_iteration_failed: bool, // Track if current iteration had condition failures
} }
/// Iterator state for different collection types /// Iterator state for different collection types.
///
/// Snapshot independence for `Object` is provided by the shared
/// `Rc<Object>` — `Rc::make_mut` on an aliased Rc allocates a new
/// collection, leaving the iterator's Rc pointing at the original
/// pre-mutation state. The `ObjectCursor` is opaque and resumes in
/// O(log n) for the BTree backend.
///
/// `Set` continues to use the pre-existing snapshot-by-cloned-key
/// approach (`current_item` + `first_iteration`); migration of `Set`
/// to a cursor-based iterator ships with the `Set` storage abstraction
/// in a follow-up PR.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum IterationState { pub enum IterationState {
Array { Array {
@@ -32,9 +44,8 @@ pub enum IterationState {
index: usize, index: usize,
}, },
Object { Object {
obj: Rc<BTreeMap<Value, Value>>, obj: Rc<Object>,
current_key: Option<Value>, cursor: ObjectCursor,
first_iteration: bool,
}, },
Set { Set {
items: Rc<BTreeSet<Value>>, items: Rc<BTreeSet<Value>>,
@@ -54,13 +65,21 @@ impl IterationState {
pub(super) const fn advance(&mut self) { pub(super) const fn advance(&mut self) {
match *self { match *self {
Self::Array { ref mut index, .. } => { Self::Array { ref mut index, .. } => {
// Array iteration uses `usize` as the cursor and advances via
// `saturating_add(1)`. A cursor already at `usize::MAX` here
// means a stuck (non-progressing) iteration was emitted by
// malformed bytecode; assert in debug to surface it loudly.
debug_assert!(
*index < usize::MAX,
"IterationState::Array index already at usize::MAX on advance"
);
*index = index.saturating_add(1); *index = index.saturating_add(1);
} }
Self::Object { // For Object the cursor advances inside `setup_next_iteration`
ref mut first_iteration, // when it pulls the next item via `Object::next`, so `advance`
.. // is a no-op for the cursor-backed Object variant.
} Self::Object { .. } => {}
| Self::Set { Self::Set {
ref mut first_iteration, ref mut first_iteration,
.. ..
} => { } => {
@@ -69,6 +88,12 @@ impl IterationState {
Self::Single { Self::Single {
ref mut consumed, .. ref mut consumed, ..
} => { } => {
// `Single` yields exactly once; advancing a consumed Single
// means the compiler emitted a redundant LoopNext.
debug_assert!(
!*consumed,
"IterationState::Single advanced after consumption"
);
*consumed = true; *consumed = true;
} }
} }
@@ -107,3 +132,71 @@ pub(super) struct ComprehensionContext {
/// Resume location for the parent frame once this comprehension completes /// Resume location for the parent frame once this comprehension completes
pub(super) resume_pc: usize, pub(super) resume_pc: usize,
} }
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::unreachable,
clippy::pattern_type_mismatch,
clippy::shadow_unrelated,
clippy::panic
)]
mod tests {
use super::*;
use crate::value::Object;
/// IterationState::Object holds an `Rc<Object>` plus an opaque cursor.
/// Mutating an aliased Rc via `Rc::make_mut` allocates a new collection
/// (CoW) so the in-flight iterator's source is unaffected.
#[test]
fn iteration_state_object_is_snapshot_independent_of_source() {
let mut obj = Object::new();
obj.insert(Value::from("a"), Value::from(1));
obj.insert(Value::from("b"), Value::from(2));
obj.insert(Value::from("c"), Value::from(3));
let source = Value::Object(Rc::new(obj));
let snapshot_obj = match &source {
Value::Object(o) => Rc::clone(o),
_ => unreachable!(),
};
let state = IterationState::Object {
obj: Rc::clone(&snapshot_obj),
cursor: snapshot_obj.cursor(),
};
// Mutate a clone of the source mid-iteration.
let mut alias = source.clone();
let inner = alias.as_object_mut().expect("object");
inner.insert(Value::from("a"), Value::from(999));
inner.insert(Value::from("d"), Value::from(4));
inner.remove(&Value::from("b"));
// Drain the snapshot via the cursor — must still report the original
// 3 entries with original values.
let mut collected: Vec<(Value, Value)> = Vec::new();
if let IterationState::Object {
ref obj,
mut cursor,
} = state
{
while let Some((k, v)) = obj.next(&mut cursor) {
collected.push((k.clone(), v.clone()));
}
} else {
unreachable!();
}
assert_eq!(collected.len(), 3);
assert!(collected.contains(&(Value::from("a"), Value::from(1))));
assert!(collected.contains(&(Value::from("b"), Value::from(2))));
assert!(collected.contains(&(Value::from("c"), Value::from(3))));
assert!(!collected.iter().any(|kv| kv.0 == Value::from("d")));
// The original source Value (untouched) is also unchanged.
let src_obj = source.as_object().expect("object");
assert_eq!(src_obj.len(), 3);
assert_eq!(src_obj.get(&Value::from("a")), Some(&Value::from(1)));
}
}
+2 -3
View File
@@ -4,7 +4,6 @@
use crate::rvm::instructions::{GuardMode, Instruction, LiteralOrRegister}; use crate::rvm::instructions::{GuardMode, Instruction, LiteralOrRegister};
use crate::rvm::program::Program; use crate::rvm::program::Program;
use crate::value::Value; use crate::value::Value;
use alloc::collections::BTreeSet;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::mem; use core::mem;
@@ -670,7 +669,7 @@ impl RegoVM {
} }
} }
SetNew { dest } => { SetNew { dest } => {
let empty_set = Value::Set(crate::Rc::new(BTreeSet::new())); let empty_set = Value::new_set();
self.set_register(dest, empty_set)?; self.set_register(dest, empty_set)?;
Ok(InstructionOutcome::Continue) Ok(InstructionOutcome::Continue)
} }
@@ -707,7 +706,7 @@ impl RegoVM {
if any_undefined { if any_undefined {
self.set_register(params.dest, Value::Undefined)?; self.set_register(params.dest, Value::Undefined)?;
} else { } else {
let mut set = BTreeSet::new(); let mut set = alloc::collections::BTreeSet::new();
for &reg in params.element_registers() { for &reg in params.element_registers() {
set.insert(self.get_register(reg)?.clone()); set.insert(self.get_register(reg)?.clone());
} }
+7
View File
@@ -295,6 +295,13 @@ pub enum VmError {
#[error("Call rule stack underflow during rule finalization (pc={pc})")] #[error("Call rule stack underflow during rule finalization (pc={pc})")]
CallRuleStackUnderflow { pc: usize }, CallRuleStackUnderflow { pc: usize },
#[error("Call rule stack mismatch during rule finalization: expected rule_index {expected}, popped {actual} (pc={pc})")]
CallRuleStackMismatch {
expected: u16,
actual: u16,
pc: usize,
},
#[error("Internal VM error: {message} (pc={pc})")] #[error("Internal VM error: {message} (pc={pc})")]
Internal { message: String, pc: usize }, Internal { message: String, pc: usize },
} }
+14
View File
@@ -117,6 +117,10 @@ impl RegoVM {
let target = self.convert_pc(target, "jump target")?; let target = self.convert_pc(target, "jump target")?;
self.pc = target; self.pc = target;
while self.pc < program.instructions.len() { while self.pc < program.instructions.len() {
// Per-instruction sanity check: every iteration of the dispatch
// loop must re-enter with the VM in a Running/Ready state and the
// working data structures coherent.
self.assert_vm_invariants();
self.memory_check()?; self.memory_check()?;
if self.executed_instructions >= self.max_instructions { if self.executed_instructions >= self.max_instructions {
return Err(VmError::InstructionLimitExceeded { return Err(VmError::InstructionLimitExceeded {
@@ -189,6 +193,9 @@ impl RegoVM {
} }
fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result<Value> { fn execute_suspendable_entry(&mut self, entry_point_pc: usize) -> Result<Value> {
// Precondition: callers (execute_entry_point_by_{index,name}) reset the
// VM before invoking this method, so the VM must be in a clean state.
self.debug_assert_state_is_clean();
self.execution_state = ExecutionState::Running; self.execution_state = ExecutionState::Running;
self.reset_execution_timer_state(); self.reset_execution_timer_state();
match self.run_stackless_from(entry_point_pc) { match self.run_stackless_from(entry_point_pc) {
@@ -201,6 +208,10 @@ impl RegoVM {
} }
pub fn resume(&mut self, resume_value: Option<Value>) -> Result<Value> { pub fn resume(&mut self, resume_value: Option<Value>) -> Result<Value> {
// Precondition is enforced below by returning `VmError::InvalidResumeState`
// for any non-`Suspended` state. A `debug_assert!` here would diverge
// debug vs release behavior and, when invoked via FFI, would trip the
// unwind guard and poison the engine on a recoverable misuse.
let (reason, mut last_result) = match self.execution_state.clone() { let (reason, mut last_result) = match self.execution_state.clone() {
ExecutionState::Suspended { ExecutionState::Suspended {
reason, reason,
@@ -289,6 +300,9 @@ impl RegoVM {
fn run_stackless_loop(&mut self, program: &Program, last_result: &mut Value) -> Result<()> { fn run_stackless_loop(&mut self, program: &Program, last_result: &mut Value) -> Result<()> {
while !self.execution_stack.is_empty() { while !self.execution_stack.is_empty() {
// Per-instruction sanity check: see `assert_vm_invariants` for the
// exact contract. Compiled out in release.
self.assert_vm_invariants();
self.memory_check()?; self.memory_check()?;
self.frame_pc_overridden = false; self.frame_pc_overridden = false;
let should_finalize_rule = self.execution_stack.last().is_some_and(|frame| { let should_finalize_rule = self.execution_stack.last().is_some_and(|frame| {
+58 -60
View File
@@ -3,6 +3,7 @@
use crate::rvm::instructions::LoopMode; use crate::rvm::instructions::LoopMode;
use crate::value::Value; use crate::value::Value;
use crate::Rc;
use super::context::{IterationState, LoopContext}; use super::context::{IterationState, LoopContext};
use super::errors::{Result, VmError}; use super::errors::{Result, VmError};
@@ -89,13 +90,13 @@ impl RegoVM {
) -> Result<()> { ) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?; self.set_register(params.result_reg, Value::Bool(false))?;
let iteration_state = match self.resolve_iteration_state(mode, &params)? { let mut iteration_state = match self.resolve_iteration_state(mode, &params)? {
Some(state) => state, Some(state) => state,
None => return Ok(()), None => return Ok(()),
}; };
let has_next = let has_next =
self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?; self.setup_next_iteration(&mut iteration_state, params.key_reg, params.value_reg)?;
if !has_next { if !has_next {
self.pc = usize::from(params.loop_end); self.pc = usize::from(params.loop_end);
return Ok(()); return Ok(());
@@ -155,15 +156,10 @@ impl RegoVM {
LoopAction::Continue => {} LoopAction::Continue => {}
} }
if let &mut IterationState::Object { // Snapshot the current value for Set so its next iteration can resume
ref mut current_key, // from `Bound::Excluded(current)`. Object uses a cursor and advances
.. // inside `setup_next_iteration` itself.
} = &mut loop_ctx.iteration_state if let &mut IterationState::Set {
{
if loop_ctx.key_reg != loop_ctx.value_reg {
*current_key = Some(self.get_register(loop_ctx.key_reg)?.clone());
}
} else if let &mut IterationState::Set {
ref mut current_item, ref mut current_item,
.. ..
} = &mut loop_ctx.iteration_state } = &mut loop_ctx.iteration_state
@@ -173,7 +169,7 @@ impl RegoVM {
loop_ctx.iteration_state.advance(); loop_ctx.iteration_state.advance();
let has_next = self.setup_next_iteration( let has_next = self.setup_next_iteration(
&loop_ctx.iteration_state, &mut loop_ctx.iteration_state,
loop_ctx.key_reg, loop_ctx.key_reg,
loop_ctx.value_reg, loop_ctx.value_reg,
)?; )?;
@@ -211,13 +207,13 @@ impl RegoVM {
) -> Result<()> { ) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?; self.set_register(params.result_reg, Value::Bool(false))?;
let iteration_state = match self.resolve_iteration_state(mode, &params)? { let mut iteration_state = match self.resolve_iteration_state(mode, &params)? {
Some(state) => state, Some(state) => state,
None => return Ok(()), None => return Ok(()),
}; };
let has_next = let has_next =
self.setup_next_iteration(&iteration_state, params.key_reg, params.value_reg)?; self.setup_next_iteration(&mut iteration_state, params.key_reg, params.value_reg)?;
if !has_next { if !has_next {
self.pc = usize::from(params.loop_end); self.pc = usize::from(params.loop_end);
return Ok(()); return Ok(());
@@ -316,7 +312,14 @@ impl RegoVM {
Ok(()) Ok(())
} }
LoopAction::Continue => { LoopAction::Continue => {
let (mode, success_count, total_iterations, key_reg, value_reg, iteration_state) = { let (
mode,
success_count,
total_iterations,
key_reg,
value_reg,
mut iteration_state,
) = {
let (mode, success_count, total_iterations, key_reg, value_reg) = { let (mode, success_count, total_iterations, key_reg, value_reg) = {
let frame = self let frame = self
.execution_stack .execution_stack
@@ -334,11 +337,6 @@ impl RegoVM {
} }
}; };
let key_value = if key_reg != value_reg {
Some(self.get_register(key_reg)?.clone())
} else {
None
};
let value_value = self.get_register(value_reg)?.clone(); let value_value = self.get_register(value_reg)?.clone();
let frame = self let frame = self
@@ -349,20 +347,16 @@ impl RegoVM {
&mut FrameKind::Loop { &mut FrameKind::Loop {
ref mut context, .. ref mut context, ..
} => { } => {
if let &mut IterationState::Object { // Snapshot the current value for Set so its next
ref mut current_key, // iteration can resume from `Bound::Excluded(current)`.
.. // Object uses a cursor and advances inside
} = &mut context.iteration_state // `setup_next_iteration` itself.
{ if let &mut IterationState::Set {
if context.key_reg != context.value_reg {
*current_key = key_value;
}
} else if let &mut IterationState::Set {
ref mut current_item, ref mut current_item,
.. ..
} = &mut context.iteration_state } = &mut context.iteration_state
{ {
*current_item = Some(value_value.clone()); *current_item = Some(value_value);
} }
context.iteration_state.advance(); context.iteration_state.advance();
@@ -381,7 +375,21 @@ impl RegoVM {
} }
}; };
let has_next = self.setup_next_iteration(&iteration_state, key_reg, value_reg)?; let has_next =
self.setup_next_iteration(&mut iteration_state, key_reg, value_reg)?;
// `setup_next_iteration` advances Object's internal cursor;
// the owning frame holds the iteration_state, so we must
// write the updated state back. (Array/Set are unchanged by
// the call, so the writeback is uniform.)
if let Some(frame) = self.execution_stack.last_mut() {
if let FrameKind::Loop {
ref mut context, ..
} = frame.kind
{
context.iteration_state = iteration_state;
}
}
if has_next { if has_next {
if let Some(frame) = self.execution_stack.last_mut() { if let Some(frame) = self.execution_stack.last_mut() {
@@ -459,10 +467,14 @@ impl RegoVM {
self.handle_empty_collection(mode, params.result_reg, params.loop_end)?; self.handle_empty_collection(mode, params.result_reg, params.loop_end)?;
return Ok(None); return Ok(None);
} }
// O(1) resumable cursor over the shared Rc<Object>.
// No eager pair snapshot: avoids O(N) setup, O(N) memory
// floor, and O(N) memory-limit checks. Snapshot
// independence is via the shared Rc (CoW).
let cursor = obj.cursor();
Ok(Some(IterationState::Object { Ok(Some(IterationState::Object {
obj: obj.clone(), obj: Rc::clone(obj),
current_key: None, cursor,
first_iteration: true,
})) }))
} }
} }
@@ -512,7 +524,7 @@ impl RegoVM {
pub(super) fn setup_next_iteration( pub(super) fn setup_next_iteration(
&mut self, &mut self,
state: &IterationState, state: &mut IterationState,
key_reg: u8, key_reg: u8,
value_reg: u8, value_reg: u8,
) -> Result<bool> { ) -> Result<bool> {
@@ -538,33 +550,19 @@ impl RegoVM {
} }
IterationState::Object { IterationState::Object {
ref obj, ref obj,
ref current_key, ref mut cursor,
ref first_iteration,
} => { } => {
if *first_iteration { // Object iterates via a resumable cursor on the shared
if let Some((key, value)) = obj.iter().next() { // `Rc<Object>`; `next` both yields the current entry and
if key_reg != value_reg { // advances the cursor. No explicit `current_key` snapshot is
self.set_register(key_reg, key.clone())?; // needed — see the doc on `IterationState`.
} if let Some((key, value)) = obj.next(cursor) {
self.set_register(value_reg, value.clone())?; let value = value.clone();
Ok(true) if key_reg != value_reg {
} else { self.set_register(key_reg, key.clone())?;
Ok(false)
}
} else if let Some(ref current) = *current_key {
let mut range_iter = obj.range((
core::ops::Bound::Excluded(current),
core::ops::Bound::Unbounded,
));
if let Some((key, value)) = range_iter.next() {
if key_reg != value_reg {
self.set_register(key_reg, key.clone())?;
}
self.set_register(value_reg, value.clone())?;
Ok(true)
} else {
Ok(false)
} }
self.set_register(value_reg, value)?;
Ok(true)
} else { } else {
Ok(false) Ok(false)
} }
+16
View File
@@ -277,6 +277,19 @@ impl RegoVM {
.call_rule_stack .call_rule_stack
.pop() .pop()
.ok_or(VmError::CallRuleStackUnderflow { pc: self.pc })?; .ok_or(VmError::CallRuleStackUnderflow { pc: self.pc })?;
// Stack discipline: the context we just popped must belong to the
// rule we are finalizing. A mismatch indicates a missing push or an
// extra pop somewhere in this rule's execution and would otherwise
// silently restore the wrong return_pc / rule_type. Surface as a
// typed VmError so the contract holds the same in debug and release
// builds (avoiding FFI poisoning via a debug-only panic).
if rule_index != call_context.rule_index {
return Err(VmError::CallRuleStackMismatch {
expected: rule_index,
actual: call_context.rule_index,
pc: self.pc,
});
}
self.pc = call_context.return_pc; self.pc = call_context.return_pc;
let result_from_rule = if !rule_failed_due_to_inconsistency { let result_from_rule = if !rule_failed_due_to_inconsistency {
@@ -831,6 +844,9 @@ impl RegoVM {
self.registers = parent_registers; self.registers = parent_registers;
// Underflow here means malformed/poisoned program state; surface as a
// typed error rather than a debug-only panic so the public load_program
// contract holds the same in debug and release.
if self.call_rule_stack.pop().is_none() { if self.call_rule_stack.pop().is_none() {
return Err(VmError::CallRuleStackUnderflow { pc: self.pc }); return Err(VmError::CallRuleStackUnderflow { pc: self.pc });
} }
+133
View File
@@ -34,6 +34,139 @@ impl RegoVM {
// Builtin cache entries only live for a single execution // Builtin cache entries only live for a single execution
self.builtins_cache.clear(); self.builtins_cache.clear();
// Postcondition: every stack/cache that `reset_execution_state` touches
// must be in its documented "clean" shape. This catches accidental
// omissions in future edits to this function.
self.debug_assert_state_is_clean();
}
/// Debug-only postcondition for `reset_execution_state`.
///
/// Asserts the invariants every caller of `reset_execution_state` relies on
/// before starting a fresh execution. The body is fully gated by
/// `#[cfg(debug_assertions)]` so this is a zero-cost no-op in release.
#[inline]
pub(super) fn debug_assert_state_is_clean(&self) {
#[cfg(debug_assertions)]
{
// --- Stacks: every per-execution stack must be drained. ---
debug_assert!(
self.execution_stack.is_empty(),
"reset_execution_state postcondition: execution_stack must be empty"
);
debug_assert!(
self.loop_stack.is_empty(),
"reset_execution_state postcondition: loop_stack must be empty"
);
debug_assert!(
self.comprehension_stack.is_empty(),
"reset_execution_state postcondition: comprehension_stack must be empty"
);
debug_assert!(
self.call_rule_stack.is_empty(),
"reset_execution_state postcondition: call_rule_stack must be empty"
);
debug_assert!(
self.register_stack.is_empty(),
"reset_execution_state postcondition: register_stack must be empty"
);
// --- Caches: cleared so a new program/input cannot read stale entries. ---
debug_assert!(
self.builtins_cache.is_empty(),
"reset_execution_state postcondition: builtins_cache must be empty"
);
// --- Registers: window resized to the program's base count and zeroed. ---
debug_assert_eq!(
self.registers.len(),
self.base_register_count,
"reset_execution_state postcondition: registers must be sized to base_register_count"
);
debug_assert!(
self.registers.iter().all(|v| matches!(v, Value::Undefined)),
"reset_execution_state postcondition: all registers must be Undefined"
);
// --- Rule cache: sized to the current program and marked uncomputed. ---
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"reset_execution_state postcondition: rule_cache size must match program rule_infos"
);
debug_assert!(
self.rule_cache.iter().all(|entry| !entry.0),
"reset_execution_state postcondition: rule_cache entries must be uncomputed"
);
// --- Counters and execution-state machine: zeroed and back to Ready. ---
debug_assert_eq!(
self.pc, 0,
"reset_execution_state postcondition: pc must be 0"
);
debug_assert_eq!(
self.executed_instructions, 0,
"reset_execution_state postcondition: executed_instructions must be 0"
);
debug_assert!(
matches!(self.execution_state, ExecutionState::Ready),
"reset_execution_state postcondition: execution_state must be Ready"
);
}
}
/// Per-opcode VM invariants checked from the inner dispatch loop.
///
/// These hold every time control re-enters the dispatch loop with another
/// instruction to execute. Only conditions that are *purely VM-internal*
/// (i.e. cannot be made false by any host-supplied program or out-of-order
/// API call) are asserted here — anything reachable from `load_program`
/// input must surface as a typed `VmError` instead, to avoid panicking in
/// debug builds and poisoning the engine across FFI.
///
/// Fully `#[cfg(debug_assertions)]`-gated so the method body compiles out
/// in release.
#[inline]
pub(super) fn assert_vm_invariants(&self) {
#[cfg(debug_assertions)]
{
// The dispatch loop only runs while execution is live. Once the VM
// has transitioned to a terminal state (Suspended/Completed/Error)
// the loop must have exited. Note `Ready` is also valid here because
// some entry points (e.g. `execute_entry_point_by_index` in
// RunToCompletion mode) drive `jump_to` without flipping the state.
// `execution_state` is mutated only inside the VM and is not
// host-controllable.
debug_assert!(
matches!(
self.execution_state,
ExecutionState::Ready | ExecutionState::Running
),
"vm invariant: execution_state must be Ready or Running inside the dispatch loop, was {:?}",
self.execution_state
);
// Rule cache is sized once at reset (against the currently loaded
// program) and the VM does not resize it mid-execution. Any
// mismatch here would indicate an internal accounting bug rather
// than malformed input.
debug_assert_eq!(
self.rule_cache.len(),
self.program.rule_infos.len(),
"vm invariant: rule_cache size must equal program.rule_infos size"
);
// NOTE: `!registers.is_empty()` and an `execution_stack` depth
// ceiling were intentionally *not* asserted here: both can be
// triggered by a host-loaded program (registers via
// `RuleInfo::num_registers == 0`; stack depth via deeply nested
// rules/loops/comprehensions) and would therefore panic in debug
// and poison the engine across FFI. Register access is already
// guarded by `VmError::RegisterIndexOutOfBounds`; runaway recursion
// is bounded in production by `set_max_instructions` and
// `memory_check`.
}
} }
/// Return all active objects to their respective pools for reuse /// Return all active objects to their respective pools for reuse
+3 -3
View File
@@ -569,7 +569,7 @@ impl Analyzer {
} }
Ok(false) Ok(false)
} }
Array { .. } | Object { .. } => Ok(true), Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => Ok(false), _ => Ok(false),
})?; })?;
Ok(true) Ok(true)
@@ -666,7 +666,7 @@ impl Analyzer {
Ok(false) Ok(false)
} }
// TODO: key vs value for object binding // TODO: key vs value for object binding
Array { .. } | Object { .. } => Ok(true), Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => Ok(false), _ => Ok(false),
})?; })?;
Ok(vars) Ok(vars)
@@ -853,7 +853,7 @@ impl Analyzer {
Ok(false) Ok(false)
} }
// TODO: Object key/value // TODO: Object key/value
Array { .. } | Object { .. } => Ok(true), Expr::Array { .. } | Expr::Object { .. } => Ok(true),
_ => { _ => {
non_vars.push(e.clone()); non_vars.push(e.clone());
Ok(false) Ok(false)
+19 -10
View File
@@ -1498,7 +1498,7 @@ fn test_deserialize_object_default_empty_object() {
let s = Schema::from_serde_json_value(schema).unwrap(); let s = Schema::from_serde_json_value(schema).unwrap();
match s.as_type() { match s.as_type() {
Type::Object { default, .. } => { Type::Object { default, .. } => {
assert_eq!(default, &Some(Value::Object(Rc::new(BTreeMap::new())))); assert_eq!(default, &Some(Value::new_object()));
} }
_ => panic!("Expected Type::Object"), _ => panic!("Expected Type::Object"),
} }
@@ -1778,8 +1778,11 @@ fn test_deserialize_enum_values_with_object_non_string_keys() {
match s.as_type() { match s.as_type() {
Type::Enum { values, .. } => match &values[0] { Type::Enum { values, .. } => match &values[0] {
Value::Object(obj) => { Value::Object(obj) => {
assert_eq!(obj[&Value::from("1")], Value::from("one")); assert_eq!(*obj.get(&Value::from("1")).expect("1"), Value::from("one"));
assert_eq!(obj[&Value::from("true")], Value::from("bool")); assert_eq!(
*obj.get(&Value::from("true")).expect("true"),
Value::from("bool")
);
} }
_ => panic!("Expected object in enum values"), _ => panic!("Expected object in enum values"),
}, },
@@ -1802,18 +1805,21 @@ fn test_deserialize_enum_values_with_deeply_nested_structures() {
match s.as_type() { match s.as_type() {
Type::Enum { values, .. } => match &values[0] { Type::Enum { values, .. } => match &values[0] {
Value::Object(obj) => { Value::Object(obj) => {
let a = &obj[&Value::from("a")]; let a = obj.get(&Value::from("a")).expect("a");
match a { match a {
Value::Array(arr) => match &arr[0] { Value::Array(arr) => match &arr[0] {
Value::Object(inner) => { Value::Object(inner) => {
let b = &inner[&Value::from("b")]; let b = inner.get(&Value::from("b")).expect("b");
match b { match b {
Value::Array(barr) => { Value::Array(barr) => {
assert_eq!(barr[0], Value::from(1)); assert_eq!(barr[0], Value::from(1));
assert_eq!(barr[1], Value::from(2)); assert_eq!(barr[1], Value::from(2));
match &barr[2] { match &barr[2] {
Value::Object(cobj) => { Value::Object(cobj) => {
assert_eq!(cobj[&Value::from("c")], Value::Null); assert_eq!(
*cobj.get(&Value::from("c")).expect("c"),
Value::Null
);
} }
_ => panic!("Expected object for 'c'"), _ => panic!("Expected object for 'c'"),
} }
@@ -1886,8 +1892,11 @@ fn test_deserialize_const_value_object() {
match s.as_type() { match s.as_type() {
Type::Const { value, .. } => match value { Type::Const { value, .. } => match value {
Value::Object(ref obj) => { Value::Object(ref obj) => {
assert_eq!(obj[&Value::from("foo")], Value::from("bar")); assert_eq!(
assert_eq!(obj[&Value::from("baz")], Value::from(1)); *obj.get(&Value::from("foo")).expect("foo"),
Value::from("bar")
);
assert_eq!(*obj.get(&Value::from("baz")).expect("baz"), Value::from(1));
} }
_ => panic!("Expected object for const value"), _ => panic!("Expected object for const value"),
}, },
@@ -1940,13 +1949,13 @@ fn test_deserialize_const_value_deeply_nested() {
match s.as_type() { match s.as_type() {
Type::Const { value, .. } => match value { Type::Const { value, .. } => match value {
Value::Object(ref obj) => { Value::Object(ref obj) => {
let a = &obj[&Value::from("a")]; let a = obj.get(&Value::from("a")).expect("a");
match a { match a {
Value::Array(arr) => { Value::Array(arr) => {
assert_eq!(arr[0], Value::from(1)); assert_eq!(arr[0], Value::from(1));
match &arr[1] { match &arr[1] {
Value::Object(inner) => { Value::Object(inner) => {
let b = &inner[&Value::from("b")]; let b = inner.get(&Value::from("b")).expect("b");
match b { match b {
Value::Array(barr) => { Value::Array(barr) => {
assert_eq!(barr[0], Value::Null); assert_eq!(barr[0], Value::Null);
+3 -2
View File
@@ -7,6 +7,7 @@
use crate::{ use crate::{
schema::{error::ValidationError, Schema, Type}, schema::{error::ValidationError, Schema, Type},
value::Object,
*, *,
}; };
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
@@ -537,7 +538,7 @@ impl SchemaValidator {
} }
fn validate_discriminated_subobject_with_base( fn validate_discriminated_subobject_with_base(
object_value: &BTreeMap<Value, Value>, object_value: &Object,
discriminated_subobject: &crate::schema::DiscriminatedSubobject, discriminated_subobject: &crate::schema::DiscriminatedSubobject,
base_properties: &BTreeMap<String, Schema>, base_properties: &BTreeMap<String, Schema>,
base_additional_properties: Option<&Schema>, base_additional_properties: Option<&Schema>,
@@ -653,7 +654,7 @@ impl SchemaValidator {
} }
fn validate_subobject( fn validate_subobject(
object_value: &BTreeMap<Value, Value>, object_value: &Object,
subobject: &crate::schema::Subobject, subobject: &crate::schema::Subobject,
path: &str, path: &str,
) -> Result<(), ValidationError> { ) -> Result<(), ValidationError> {
+1 -1
View File
@@ -88,7 +88,7 @@ fn analyze_file(regos: &[String], expected_scopes: &[Scope]) -> Result<()> {
} }
} }
scopes.sort_by(|a, b| a.0.span.line.cmp(&b.0.span.line)); scopes.sort_by_key(|a| a.0.span.line);
for (idx, (_, scope)) in scopes.iter().enumerate() { for (idx, (_, scope)) in scopes.iter().enumerate() {
if idx > expected_scopes.len() { if idx > expected_scopes.len() {
bail!("extra scope generated.") bail!("extra scope generated.")
+27 -26
View File
@@ -11,6 +11,18 @@
clippy::as_conversions clippy::as_conversions
)] // value helpers index paths directly for performance )] // value helpers index paths directly for performance
mod object;
#[cfg(test)]
mod tests;
#[allow(unused_imports)] // surface for downstream PRs
pub use object::{IntoIter, Iter, IterMut, Object};
#[cfg(feature = "rvm")]
#[allow(unused_imports)] // surface for downstream PRs
pub use object::ObjectCursor;
use crate::number::Number; use crate::number::Number;
use alloc::collections::{BTreeMap, BTreeSet}; use alloc::collections::{BTreeMap, BTreeSet};
@@ -23,7 +35,7 @@ use core::str::FromStr;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use serde::de::{self, Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor}; use serde::de::{self, Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor};
use serde::ser::{SerializeMap, Serializer}; use serde::ser::Serializer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::*; use crate::*;
@@ -63,7 +75,7 @@ pub enum Value {
/// An object. /// An object.
/// Unlike JSON, keys can be any value, not just string. /// Unlike JSON, keys can be any value, not just string.
Object(Rc<BTreeMap<Value, Value>>), Object(Rc<Object>),
/// Undefined value. /// Undefined value.
/// Used to indicate the absence of a value. /// Used to indicate the absence of a value.
@@ -86,26 +98,15 @@ impl Serialize for Value {
where where
S: Serializer, S: Serializer,
{ {
use serde::ser::Error;
match self { match self {
Value::Null => serializer.serialize_unit(), Value::Null => serializer.serialize_unit(),
Value::Bool(b) => serializer.serialize_bool(*b), Value::Bool(b) => serializer.serialize_bool(*b),
Value::String(s) => serializer.serialize_str(s.as_ref()), Value::String(s) => serializer.serialize_str(s.as_ref()),
Value::Number(n) => n.serialize(serializer), Value::Number(n) => n.serialize(serializer),
Value::Array(a) => a.serialize(serializer), Value::Array(a) => a.serialize(serializer),
Value::Object(fields) => { // Delegate to the Object/Set serializers — single canonical path,
let mut map = serializer.serialize_map(Some(fields.len()))?; // handles non-string-key stringification internally.
for (k, v) in fields.iter() { Value::Object(fields) => fields.serialize(serializer),
match k {
Value::String(_) => map.serialize_entry(k, v)?,
_ => {
let key_str = serde_json::to_string(k).map_err(Error::custom)?;
map.serialize_entry(&key_str, v)?
}
}
}
map.end()
}
// display set as an array // display set as an array
Value::Set(s) => s.serialize(serializer), Value::Set(s) => s.serialize(serializer),
@@ -345,7 +346,7 @@ impl Value {
/// assert_eq!(array[4], Value::from(12345u64)); /// assert_eq!(array[4], Value::from(12345u64));
/// let obj = array[5].as_object().expect("not an object"); /// let obj = array[5].as_object().expect("not an object");
/// assert_eq!(obj.len(), 1); /// assert_eq!(obj.len(), 1);
/// assert_eq!(obj[&Value::from("name")], Value::from("regorus")); /// assert_eq!(obj.get(&Value::from("name")).expect("missing name"), &Value::from("regorus"));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -800,7 +801,7 @@ impl From<BTreeMap<Value, Value>> for Value {
/// # Ok(()) /// # Ok(())
/// # } /// # }
fn from(s: BTreeMap<Value, Value>) -> Self { fn from(s: BTreeMap<Value, Value>) -> Self {
Value::Object(Rc::new(s)) Value::Object(Rc::new(Object::from(s)))
} }
} }
@@ -1279,16 +1280,16 @@ impl Value {
} }
} }
/// Cast value to [`& BTreeMap<Value, Value>`] if [`Value::Object`]. /// Cast value to [`&Object`] if [`Value::Object`].
/// ``` /// ```
/// # use regorus::*; /// # use regorus::*;
/// # use std::collections::BTreeMap; /// # use regorus::value::Object;
/// # fn main() -> anyhow::Result<()> { /// # fn main() -> anyhow::Result<()> {
/// let v = Value::from( /// let v = Value::from(
/// [(Value::from("Hello"), Value::from("World"))] /// [(Value::from("Hello"), Value::from("World"))]
/// .iter() /// .iter()
/// .cloned() /// .cloned()
/// .collect::<BTreeMap<Value, Value>>(), /// .collect::<Object>(),
/// ); /// );
/// assert_eq!( /// assert_eq!(
/// v.as_object()?.iter().next(), /// v.as_object()?.iter().next(),
@@ -1296,28 +1297,28 @@ impl Value {
/// ); /// );
/// # Ok(()) /// # Ok(())
/// # } /// # }
pub fn as_object(&self) -> Result<&BTreeMap<Value, Value>> { pub fn as_object(&self) -> Result<&Object> {
match self { match self {
Value::Object(m) => Ok(m), Value::Object(m) => Ok(m),
_ => Err(anyhow!("not an object")), _ => Err(anyhow!("not an object")),
} }
} }
/// Cast value to [`&mut BTreeMap<Value, Value>`] if [`Value::Object`]. /// Cast value to [`&mut Object`] if [`Value::Object`].
/// ``` /// ```
/// # use regorus::*; /// # use regorus::*;
/// # use std::collections::BTreeMap; /// # use regorus::value::Object;
/// # fn main() -> anyhow::Result<()> { /// # fn main() -> anyhow::Result<()> {
/// let mut v = Value::from( /// let mut v = Value::from(
/// [(Value::from("Hello"), Value::from("World"))] /// [(Value::from("Hello"), Value::from("World"))]
/// .iter() /// .iter()
/// .cloned() /// .cloned()
/// .collect::<BTreeMap<Value, Value>>(), /// .collect::<Object>(),
/// ); /// );
/// v.as_object_mut()?.insert(Value::from("Good"), Value::from("Bye")); /// v.as_object_mut()?.insert(Value::from("Good"), Value::from("Bye"));
/// # Ok(()) /// # Ok(())
/// # } /// # }
pub fn as_object_mut(&mut self) -> Result<&mut BTreeMap<Value, Value>> { pub fn as_object_mut(&mut self) -> Result<&mut Object> {
match self { match self {
Value::Object(m) => Ok(Rc::make_mut(m)), Value::Object(m) => Ok(Rc::make_mut(m)),
_ => Err(anyhow!("not an object")), _ => Err(anyhow!("not an object")),
+148
View File
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Opaque iterator types for [`Object`].
//!
//! These newtypes wrap the storage backend's iterators so the backend can be
//! swapped without changing any iterator type signatures observed by callers.
use alloc::collections::btree_map;
use core::iter::FusedIterator;
use super::Object;
use crate::value::Value;
/// Owned iterator over `(Value, Value)` entries.
#[derive(Debug)]
pub struct IntoIter {
pub(super) inner: btree_map::IntoIter<Value, Value>,
}
impl Iterator for IntoIter {
type Item = (Value, Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for IntoIter {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl ExactSizeIterator for IntoIter {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl FusedIterator for IntoIter {}
/// Borrowed iterator over `(&Value, &Value)` entries.
#[derive(Debug, Clone)]
pub struct Iter<'a> {
pub(super) inner: btree_map::Iter<'a, Value, Value>,
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a Value, &'a Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for Iter<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<'a> ExactSizeIterator for Iter<'a> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> FusedIterator for Iter<'a> {}
/// Borrowed iterator over `(&Value, &mut Value)` entries.
#[derive(Debug)]
pub struct IterMut<'a> {
pub(super) inner: btree_map::IterMut<'a, Value, Value>,
}
impl<'a> Iterator for IterMut<'a> {
type Item = (&'a Value, &'a mut Value);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a> DoubleEndedIterator for IterMut<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back()
}
}
impl<'a> ExactSizeIterator for IterMut<'a> {
#[inline]
fn len(&self) -> usize {
self.inner.len()
}
}
impl<'a> FusedIterator for IterMut<'a> {}
impl IntoIterator for Object {
type Item = (Value, Value);
type IntoIter = IntoIter;
#[inline]
fn into_iter(self) -> Self::IntoIter {
IntoIter {
inner: self.inner.into_iter(),
}
}
}
impl<'a> IntoIterator for &'a Object {
type Item = (&'a Value, &'a Value);
type IntoIter = Iter<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
Iter {
inner: self.inner.iter(),
}
}
}
impl<'a> IntoIterator for &'a mut Object {
type Item = (&'a Value, &'a mut Value);
type IntoIter = IterMut<'a>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
IterMut {
inner: self.inner.iter_mut(),
}
}
}
+265
View File
@@ -0,0 +1,265 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! See [`Object`].
mod iter;
mod serde;
use alloc::collections::BTreeMap;
use core::cmp::Ordering;
use core::fmt;
use core::ops::Bound;
use crate::value::Value;
pub use iter::{IntoIter, Iter, IterMut};
/// Opaque, ordered key-value map keyed by [`Value`].
///
/// The current backing storage is `BTreeMap<Value, Value>`. The inner field
/// is private so the representation can change (two-tier inline+hash, lazy,
/// schema-shared) without touching call sites.
///
/// # Iteration
///
/// - [`Object::iter`] — implementation-defined order; non-resumable.
/// - [`Object::iter_sorted`] — sorted by `Value::Ord`; non-resumable.
/// - [`Object::cursor`] / [`Object::next`] — implementation-defined order,
/// resumable; cheapest per-step cost. Used by interpreter/RVM when iteration
/// must yield mid-flight.
#[derive(Default, Clone, Eq, PartialEq)]
pub struct Object {
inner: BTreeMap<Value, Value>,
}
impl Object {
/// Create an empty `Object`.
#[inline]
pub const fn new() -> Self {
Self {
inner: BTreeMap::new(),
}
}
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[inline]
pub fn get(&self, key: &Value) -> Option<&Value> {
self.inner.get(key)
}
#[inline]
pub fn contains_key(&self, key: &Value) -> bool {
self.inner.contains_key(key)
}
#[inline]
pub fn get_mut(&mut self, key: &Value) -> Option<&mut Value> {
self.inner.get_mut(key)
}
/// Iteration in implementation-defined order. Non-resumable.
///
/// For the current BTree-backed storage this happens to be sorted, but
/// callers MUST NOT depend on that. Use [`Object::iter_sorted`] when
/// deterministic order is required, or [`Object::cursor`] when iteration
/// must yield and resume.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&Value, &Value)> + '_ {
self.inner.iter()
}
/// Iteration in sorted key order (by `Value::Ord`). Non-resumable.
///
/// Use this for serialization, snapshots, hashing, `Debug`, the
/// `object.keys` builtin, etc.
#[inline]
pub fn iter_sorted(&self) -> Iter<'_> {
// BTree backend iterates sorted natively.
Iter {
inner: self.inner.iter(),
}
}
#[inline]
pub fn keys(&self) -> impl Iterator<Item = &Value> + '_ {
self.inner.keys()
}
/// Keys in sorted order (by `Value::Ord`). Symmetric with
/// [`Object::iter_sorted`].
#[inline]
pub fn keys_sorted(&self) -> impl Iterator<Item = &Value> + '_ {
self.iter_sorted().map(|(k, _)| k)
}
#[inline]
pub fn values(&self) -> impl Iterator<Item = &Value> + '_ {
self.inner.values()
}
#[inline]
pub fn iter_mut(&mut self) -> IterMut<'_> {
IterMut {
inner: self.inner.iter_mut(),
}
}
/// Insert a key-value pair. Returns the previous value if any.
#[inline]
pub fn insert(&mut self, key: Value, value: Value) -> Option<Value> {
self.inner.insert(key, value)
}
#[inline]
pub fn remove(&mut self, key: &Value) -> Option<Value> {
self.inner.remove(key)
}
#[inline]
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&Value, &mut Value) -> bool,
{
self.inner.retain(f);
}
#[inline]
pub fn clear(&mut self) {
self.inner.clear();
}
#[inline]
pub fn append(&mut self, other: &mut Object) {
self.inner.append(&mut other.inner);
}
/// Gets a mutable reference to the value associated with `key`, inserting
/// the result of `default()` if absent. Single O(log n) probe.
pub fn get_or_insert_with<F: FnOnce() -> Value>(
&mut self,
key: Value,
default: F,
) -> &mut Value {
self.inner.entry(key).or_insert_with(default)
}
/// Wrap into a `Value::Object`.
#[inline]
pub fn into_value(self) -> Value {
Value::Object(crate::Rc::new(self))
}
/// Create a resumable cursor over entries in implementation-defined
/// order. Stable for the lifetime of `&self`. O(1).
///
/// The cursor is fully self-owned (it stores a clone of the last-seen
/// key, not a reference) so it can be stored as a field of a
/// long-lived state struct — e.g. an RVM iteration frame that persists
/// across instruction dispatches. As a consequence, mutating the
/// `Object` between `next()` calls is not rejected by the borrow
/// checker; the resulting iteration order in that case is unspecified.
#[inline]
pub const fn cursor(&self) -> ObjectCursor {
ObjectCursor {
inner: ObjectCursorInner::BTree(None),
}
}
/// Advance `cursor` and yield the next entry. O(log n) for the BTree
/// backend (range probe); future hash/inline variants may be O(1).
pub fn next<'a>(&'a self, cursor: &mut ObjectCursor) -> Option<(&'a Value, &'a Value)> {
let ObjectCursorInner::BTree(ref mut last) = cursor.inner;
let next = last.as_ref().map_or_else(
|| self.inner.iter().next(),
|prev| {
self.inner
.range((Bound::Excluded(prev.clone()), Bound::Unbounded))
.next()
},
);
let (k, v) = next?;
*last = Some(k.clone());
Some((k, v))
}
}
/// Opaque resumable cursor over an [`Object`]'s entries in
/// implementation-defined order.
///
/// Self-owned: holds no borrow on the `Object`, so it can be stored as a
/// field of a long-lived state struct (e.g. an RVM iteration frame).
#[derive(Debug, Clone)]
pub struct ObjectCursor {
inner: ObjectCursorInner,
}
#[derive(Debug, Clone)]
enum ObjectCursorInner {
/// BTree backend cursor: tracks last-seen key. `None` means "before start".
BTree(Option<Value>),
}
// ---- Hand-written Ord/PartialOrd ----------------------------------------
//
// Implemented in terms of `iter_sorted()` so ordering is consistent with the
// canonical (sorted) view of the entries and is therefore independent of
// the storage variant.
impl Ord for Object {
fn cmp(&self, other: &Self) -> Ordering {
self.iter_sorted().cmp(other.iter_sorted())
}
}
impl PartialOrd for Object {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Debug for Object {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Use sorted iteration so Debug output is stable across storage
// variants.
f.debug_map().entries(self.iter_sorted()).finish()
}
}
impl Extend<(Value, Value)> for Object {
fn extend<I: IntoIterator<Item = (Value, Value)>>(&mut self, iter: I) {
self.inner.extend(iter);
}
}
impl FromIterator<(Value, Value)> for Object {
fn from_iter<I: IntoIterator<Item = (Value, Value)>>(iter: I) -> Self {
Self {
inner: BTreeMap::from_iter(iter),
}
}
}
impl From<BTreeMap<Value, Value>> for Object {
#[inline]
fn from(map: BTreeMap<Value, Value>) -> Self {
Self { inner: map }
}
}
impl From<Object> for Value {
#[inline]
fn from(o: Object) -> Self {
o.into_value()
}
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Serde `Serialize`/`Deserialize` impls for [`Object`].
use alloc::string::ToString as _;
use core::fmt;
use serde::de::{Deserialize, Deserializer, Error as _, MapAccess, Visitor};
use serde::ser::{Serialize, SerializeMap as _, Serializer};
use super::Object;
use crate::value::Value;
impl Serialize for Object {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::Error;
let mut map = serializer.serialize_map(Some(self.len()))?;
// Sorted iteration: canonical JSON.
for (k, v) in self.iter_sorted() {
match *k {
Value::String(_) => map.serialize_entry(k, v)?,
_ => {
// Non-string keys are stringified via serde_json::to_string
// so the resulting JSON has valid string keys.
let key_str = serde_json::to_string(k).map_err(Error::custom)?;
map.serialize_entry(&key_str, v)?;
}
}
}
map.end()
}
}
struct ObjectVisitor;
impl<'de> Visitor<'de> for ObjectVisitor {
type Value = Object;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a map of Value to Value")
}
fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
let mut obj = Object::new();
while let Some((k, v)) = access.next_entry::<Value, Value>()? {
obj.insert(k, v);
crate::utils::limits::check_memory_limit_if_needed()
.map_err(|err| A::Error::custom(err.to_string()))?;
}
Ok(obj)
}
}
impl<'de> Deserialize<'de> for Object {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(ObjectVisitor)
}
}
+564
View File
@@ -0,0 +1,564 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::as_conversions,
clippy::arithmetic_side_effects,
clippy::unseparated_literal_suffix,
clippy::map_unwrap_or,
clippy::option_if_let_else,
clippy::pattern_type_mismatch
)]
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::vec::Vec;
use super::Object;
use crate::value::Value;
fn val(i: u64) -> Value {
Value::from(i)
}
fn make_pairs(n: u64) -> Vec<(Value, Value)> {
(0..n).map(|i| (val(i), val(i.saturating_mul(2)))).collect()
}
const SIZES: &[u64] = &[0, 1, 2, 4, 8, 64, 256, 1024];
/// `iter_sorted` must yield entries in the same order as a `BTreeMap` oracle.
#[test]
fn object_iter_sorted_matches_btreemap_oracle() {
for &n in SIZES {
let pairs = make_pairs(n);
let oracle: BTreeMap<Value, Value> = pairs.iter().cloned().collect();
let obj: Object = pairs.into_iter().collect();
let actual: Vec<(&Value, &Value)> = obj.iter_sorted().collect();
let expected: Vec<(&Value, &Value)> = oracle.iter().collect();
assert_eq!(actual, expected, "size {n}");
}
}
/// `iter` may be in any order, but as a multiset must equal the oracle's entries.
#[test]
fn object_iter_multiset_equality_with_oracle() {
for &n in SIZES {
let pairs = make_pairs(n);
let oracle: BTreeMap<Value, Value> = pairs.iter().cloned().collect();
let obj: Object = pairs.into_iter().collect();
assert_eq!(obj.len(), oracle.len(), "size {n}");
let mut a: Vec<(Value, Value)> = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let mut b: Vec<(Value, Value)> =
oracle.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
a.sort();
b.sort();
assert_eq!(a, b);
}
}
/// Serialize-then-deserialize must round-trip through JSON without loss.
#[test]
fn object_serde_roundtrip() {
for &n in &[0_u64, 1, 8, 64] {
let pairs: Vec<(Value, Value)> = (0..n)
.map(|i| (Value::String(format!("k{i}").into()), val(i)))
.collect();
let obj: Object = pairs.into_iter().collect();
let json = serde_json::to_string(&obj).expect("ser");
let back: Object = serde_json::from_str(&json).expect("de");
assert_eq!(obj, back, "size {n}");
}
}
/// Equality depends only on contents, not the order keys were inserted.
#[test]
fn object_eq_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..32_u64 {
a.insert(val(i), val(i.saturating_add(1)));
}
for i in (0..32_u64).rev() {
b.insert(val(i), val(i.saturating_add(1)));
}
assert_eq!(a, b);
}
/// `remove` returns the prior value (or `None`) and `retain` keeps only matching entries.
#[test]
fn object_remove_and_retain() {
let mut obj: Object = make_pairs(16).into_iter().collect();
assert_eq!(obj.remove(&val(0)), Some(val(0)));
assert!(obj.remove(&val(100)).is_none());
obj.retain(|_, v| {
if let Value::Number(ref n) = *v {
n.as_u64().is_some_and(|x| x % 4 == 0)
} else {
false
}
});
for (_, v) in obj.iter_sorted() {
if let Value::Number(ref n) = *v {
assert_eq!(n.as_u64().expect("u64") % 4, 0);
}
}
}
/// `IntoIterator` for `Object` (by value) yields every entry exactly once.
#[test]
fn object_into_iterator_owned() {
let obj: Object = make_pairs(8).into_iter().collect();
let collected: Vec<(Value, Value)> = obj.into_iter().collect();
assert_eq!(collected.len(), 8);
}
// ---- Duplicate-key semantics --------------------------------------------
/// `FromIterator` keeps the last value when the same key appears multiple times.
#[test]
fn object_from_iter_last_wins_on_duplicate_keys() {
let obj = Object::from_iter([(val(0), val(1)), (val(0), val(2))]);
assert_eq!(obj.get(&val(0)), Some(&val(2)));
assert_eq!(obj.len(), 1);
}
/// `From<BTreeMap>` adopts `BTreeMap`'s own last-write-wins semantics for duplicates.
#[test]
fn object_from_btreemap_last_wins_on_duplicate_keys() {
let mut bm: BTreeMap<Value, Value> = BTreeMap::new();
bm.insert(val(0), val(1));
bm.insert(val(0), val(2));
let obj: Object = bm.into();
assert_eq!(obj.get(&val(0)), Some(&val(2)));
assert_eq!(obj.len(), 1);
}
// ---- get_or_insert_with --------------------------------------------------
/// `get_or_insert_with` inserts the default when the key is absent and returns a mutable ref to it.
#[test]
fn object_get_or_insert_with_inserts_when_absent() {
let mut obj = Object::new();
let v = obj.get_or_insert_with(val(7), || val(42));
assert_eq!(*v, val(42));
*v = val(43);
assert_eq!(obj.get(&val(7)), Some(&val(43)));
}
/// `get_or_insert_with` returns the existing value and never invokes the default closure.
#[test]
fn object_get_or_insert_with_returns_existing_when_present() {
let mut obj = Object::new();
obj.insert(val(7), val(1));
let mut closure_called = false;
let v = obj.get_or_insert_with(val(7), || {
closure_called = true;
val(999)
});
assert_eq!(*v, val(1));
assert!(!closure_called, "default closure must not run when present");
}
// ---- Accessor coverage ---------------------------------------------------
/// Smoke-test every accessor: `contains_key`/`get`/`get_mut`/`keys`/`values`/`iter`/`iter_mut`/`append`/`clear`.
#[test]
fn object_accessor_coverage() {
let mut obj: Object = make_pairs(4).into_iter().collect();
assert!(obj.contains_key(&val(0)));
assert!(!obj.contains_key(&val(100)));
assert_eq!(obj.get(&val(2)), Some(&val(4)));
if let Some(v) = obj.get_mut(&val(1)) {
*v = val(999);
}
assert_eq!(obj.get(&val(1)), Some(&val(999)));
let keys: Vec<&Value> = obj.keys().collect();
assert_eq!(keys.len(), 4);
let values: Vec<&Value> = obj.values().collect();
assert_eq!(values.len(), 4);
for (_, v) in obj.iter_mut() {
*v = val(0);
}
for (_, v) in obj.iter() {
assert_eq!(*v, val(0));
}
let mut other = Object::new();
other.insert(val(100), val(200));
obj.append(&mut other);
assert!(other.is_empty());
assert!(obj.contains_key(&val(100)));
obj.clear();
assert!(obj.is_empty());
}
// ---- IntoIterator for references -----------------------------------------
/// `IntoIterator` for `&Object` yields shared refs to every entry.
#[test]
fn object_into_iterator_ref() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut count = 0;
for (_k, _v) in &obj {
count += 1;
}
assert_eq!(count, 4);
}
/// `IntoIterator` for `&mut Object` exposes mutable refs to values; mutations persist.
#[test]
fn object_into_iterator_ref_mut() {
let mut obj: Object = make_pairs(4).into_iter().collect();
for (_k, v) in &mut obj {
*v = val(0);
}
for (_, v) in obj.iter() {
assert_eq!(*v, val(0));
}
}
// ---- Cursor tests --------------------------------------------------------
/// Driving `cursor`+`next` to completion visits each entry exactly once.
#[test]
fn object_cursor_yields_every_entry_once() {
for &n in SIZES {
let pairs = make_pairs(n);
let obj: Object = pairs.clone().into_iter().collect();
let mut cursor = obj.cursor();
let mut collected: Vec<(Value, Value)> = Vec::new();
while let Some((k, v)) = obj.next(&mut cursor) {
collected.push((k.clone(), v.clone()));
}
let mut a = collected;
a.sort();
let mut b = pairs;
b.sort();
assert_eq!(a, b, "size {n}");
}
}
/// A freshly-constructed cursor restarts from the beginning, independent of any prior cursor's state.
#[test]
fn object_cursor_resumable_fresh_cursor_restarts() {
let obj: Object = make_pairs(8).into_iter().collect();
let mut c1 = obj.cursor();
let _ = obj.next(&mut c1);
let _ = obj.next(&mut c1);
let mut c2 = obj.cursor();
let first_again = obj.next(&mut c2);
let first_original = obj.iter().next();
assert_eq!(
first_again.map(|(k, v)| (k.clone(), v.clone())),
first_original.map(|(k, v)| (k.clone(), v.clone()))
);
}
/// When `Object` is shared via `Rc`, `Rc::make_mut` clones — leaving an in-flight cursor on the original snapshot unaffected.
#[test]
fn object_cursor_snapshot_independence_via_rc() {
use crate::Rc;
let mut obj = Object::new();
obj.insert(Value::from("a"), Value::from(1));
obj.insert(Value::from("b"), Value::from(2));
obj.insert(Value::from("c"), Value::from(3));
let rc_obj = Rc::new(obj);
let alias = Rc::clone(&rc_obj);
let mut cursor = rc_obj.cursor();
let _ = rc_obj.next(&mut cursor);
let mut alias_for_mut = alias;
Rc::make_mut(&mut alias_for_mut).insert(Value::from("d"), Value::from(4));
Rc::make_mut(&mut alias_for_mut).remove(&Value::from("a"));
assert_eq!(rc_obj.len(), 3);
let mut remaining = 0;
while rc_obj.next(&mut cursor).is_some() {
remaining += 1;
}
assert_eq!(remaining, 2);
}
/// A cursor over an empty `Object` returns `None` on the first call.
#[test]
fn object_cursor_empty_returns_none_immediately() {
let obj = Object::new();
let mut cursor = obj.cursor();
assert!(obj.next(&mut cursor).is_none());
}
/// Mutating an `Object` between `next()` calls is well-defined: the cursor
/// must not panic and must terminate. The visit order, and whether
/// inserted/removed keys appear, is intentionally unspecified — this test
/// only pins the safety + termination guarantees that callers (e.g. a
/// future RVM iteration frame) may rely on. It must NOT assert any
/// particular order or count, or future backend swaps will be forced to
/// honor an accidental contract.
#[test]
fn object_cursor_mutation_between_steps_is_safe_and_terminates() {
let mut obj: Object = make_pairs(16).into_iter().collect();
let mut cursor = obj.cursor();
// Yield a few entries before mutating.
for _ in 0..3 {
let _ = obj.next(&mut cursor);
}
// Interleave mutations and steps. Each yielded entry must, at the
// moment of yield, be a real entry in the map.
obj.insert(val(100), val(100));
if let Some((k, v)) = obj.next(&mut cursor) {
assert_eq!(obj.get(k), Some(v));
}
obj.remove(&val(2));
if let Some((k, v)) = obj.next(&mut cursor) {
assert_eq!(obj.get(k), Some(v));
}
obj.clear();
// After clear(), draining the cursor must terminate (not panic, not
// loop) within a bounded number of calls.
let mut terminated = false;
for _ in 0..32 {
if obj.next(&mut cursor).is_none() {
terminated = true;
break;
}
}
assert!(terminated, "cursor failed to terminate after clear()");
}
// ---- Hand-written Ord consistency ---------------------------------------
/// `Ord` (built atop `iter_sorted`) is invariant to insertion order.
#[test]
fn object_ord_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..16_u64 {
a.insert(val(i), val(i.saturating_add(1)));
}
for i in (0..16_u64).rev() {
b.insert(val(i), val(i.saturating_add(1)));
}
use core::cmp::Ordering;
assert_eq!(a.cmp(&b), Ordering::Equal);
}
/// `Ord` agrees with lexicographic comparison of the sorted-entries view.
#[test]
fn object_ord_lexicographic_on_sorted_entries() {
let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect();
assert!(a < b);
}
/// `empty < non_empty` and a shorter prefix compares less than its extension.
#[test]
fn object_ord_empty_and_prefix() {
use core::cmp::Ordering;
let empty = Object::new();
let one: Object = [(val(0), val(0))].into_iter().collect();
let two: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
assert_eq!(empty.cmp(&one), Ordering::Less);
assert_eq!(one.cmp(&two), Ordering::Less);
assert_eq!(two.cmp(&empty), Ordering::Greater);
}
/// When keys match, `Ord` falls through to comparing values.
#[test]
fn object_ord_breaks_ties_on_values() {
use core::cmp::Ordering;
let a: Object = [(val(0), val(1))].into_iter().collect();
let b: Object = [(val(0), val(2))].into_iter().collect();
assert_eq!(a.cmp(&b), Ordering::Less);
}
/// `PartialOrd` must agree with `Ord` for every input pair.
#[test]
fn object_partial_cmp_matches_cmp() {
let a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let b: Object = [(val(0), val(0)), (val(2), val(2))].into_iter().collect();
assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)));
assert_eq!(b.partial_cmp(&a), Some(b.cmp(&a)));
assert_eq!(a.partial_cmp(&a), Some(core::cmp::Ordering::Equal));
}
// ---- Debug / keys_sorted determinism ------------------------------------
/// `Debug` output is byte-identical for equal Objects regardless of insertion order.
#[test]
fn object_debug_invariant_to_insertion_order() {
let mut a = Object::new();
let mut b = Object::new();
for i in 0..8_u64 {
a.insert(val(i), val(i));
}
for i in (0..8_u64).rev() {
b.insert(val(i), val(i));
}
assert_eq!(format!("{a:?}"), format!("{b:?}"));
}
/// `keys_sorted` yields exactly `iter_sorted().map(|(k,_)| k)`.
#[test]
fn object_keys_sorted_matches_iter_sorted_keys() {
let obj: Object = make_pairs(16).into_iter().collect();
let from_keys: Vec<&Value> = obj.keys_sorted().collect();
let from_iter: Vec<&Value> = obj.iter_sorted().map(|(k, _)| k).collect();
assert_eq!(from_keys, from_iter);
}
// ---- Serde: non-string keys & determinism --------------------------------
/// `Serialize` stringifies non-string keys, and equal Objects produce identical JSON
/// regardless of insertion order.
#[test]
fn object_serialize_non_string_keys_and_deterministic() {
let pairs = [
(Value::from("alpha"), val(1)),
(Value::Bool(true), val(2)),
(val(7), val(3)),
];
let a: Object = pairs.iter().cloned().collect();
let mut b = Object::new();
for (k, v) in pairs.iter().rev().cloned() {
b.insert(k, v);
}
let ja = serde_json::to_string(&a).expect("ser a");
let jb = serde_json::to_string(&b).expect("ser b");
assert_eq!(ja, jb, "serialization must be deterministic");
// Non-string keys appear as quoted strings in the resulting JSON.
let v: serde_json::Value = serde_json::from_str(&ja).expect("parse");
let obj = v.as_object().expect("json object");
assert!(
obj.contains_key("true"),
"bool key was not stringified: {ja}"
);
assert!(
obj.contains_key("7"),
"number key was not stringified: {ja}"
);
assert!(obj.contains_key("alpha"));
}
// ---- Extend / append duplicate-key semantics -----------------------------
/// `extend` overwrites existing entries (last-write-wins) and preserves length when
/// only existing keys are touched.
#[test]
fn object_extend_last_wins_and_empty_noop() {
let mut obj: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
obj.extend([(val(0), val(99))]);
assert_eq!(obj.get(&val(0)), Some(&val(99)));
assert_eq!(obj.len(), 2);
let before = obj.len();
obj.extend(core::iter::empty::<(Value, Value)>());
assert_eq!(obj.len(), before, "empty extend is a no-op");
}
/// `append` drains `other` into `self`, overwriting on overlapping keys.
#[test]
fn object_append_overlapping_keys_drain_and_overwrite() {
let mut a: Object = [(val(0), val(0)), (val(1), val(1))].into_iter().collect();
let mut b: Object = [(val(1), val(99)), (val(2), val(2))].into_iter().collect();
a.append(&mut b);
assert!(b.is_empty(), "append must drain `other`");
assert_eq!(a.len(), 3);
assert_eq!(a.get(&val(1)), Some(&val(99)));
assert_eq!(a.get(&val(2)), Some(&val(2)));
}
// ---- Iterator trait surface ---------------------------------------------
/// `DoubleEndedIterator`/`ExactSizeIterator`/`FusedIterator` and `size_hint` all
/// behave correctly across partial consumption from both ends.
#[test]
fn object_iter_sorted_double_ended_and_exact_size() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut it = obj.iter_sorted();
assert_eq!(it.len(), 4);
assert_eq!(it.size_hint(), (4, Some(4)));
let first = it.next().expect("front");
let last = it.next_back().expect("back");
assert_eq!(it.len(), 2);
assert_eq!(it.size_hint(), (2, Some(2)));
assert_ne!(first.0, last.0, "front and back must differ for n=4");
// Drain remaining.
while it.next().is_some() {}
assert_eq!(it.len(), 0);
// FusedIterator: stays None after exhaustion.
assert!(it.next().is_none());
assert!(it.next().is_none());
assert!(it.next_back().is_none());
}
/// `IntoIter` also honors `DoubleEndedIterator` and `ExactSizeIterator`.
#[test]
fn object_into_iter_double_ended_and_exact_size() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut it = obj.into_iter();
assert_eq!(it.len(), 4);
let _ = it.next().expect("front");
let _ = it.next_back().expect("back");
assert_eq!(it.len(), 2);
let collected: Vec<_> = it.collect();
assert_eq!(collected.len(), 2);
}
/// `IterMut` decrements its `len()` after consuming from the front.
#[test]
fn object_iter_mut_exact_size() {
let mut obj: Object = make_pairs(3).into_iter().collect();
let mut it = obj.iter_mut();
assert_eq!(it.len(), 3);
let _ = it.next().expect("front");
assert_eq!(it.len(), 2);
}
/// `Iter` is `Clone`; the clone iterates independently from the same point.
#[test]
fn object_iter_sorted_clone_is_independent() {
let obj: Object = make_pairs(4).into_iter().collect();
let mut a = obj.iter_sorted();
let _ = a.next();
let b = a.clone();
let rest_a: Vec<_> = a.collect();
let rest_b: Vec<_> = b.collect();
assert_eq!(rest_a, rest_b);
}
// ---- default / insert ---------------------------------------------------
/// `Object::default()` and `Object::new()` produce equal, empty Objects.
#[test]
fn object_default_equals_new_and_is_empty() {
let a = Object::default();
let b = Object::new();
assert_eq!(a, b);
assert!(a.is_empty());
assert_eq!(a.len(), 0);
}
/// `insert` returns `None` for a fresh key and `Some(old)` when overwriting.
#[test]
fn object_insert_returns_previous_value() {
let mut obj = Object::new();
assert_eq!(obj.insert(val(0), val(1)), None);
assert_eq!(obj.insert(val(0), val(2)), Some(val(1)));
assert_eq!(obj.get(&val(0)), Some(&val(2)));
}
+3 -5
View File
@@ -544,16 +544,14 @@ fn make_context(case: &TestCase) -> Result<Value> {
let map = ctx.as_object_mut()?; let map = ctx.as_object_mut()?;
// Only inject if the caller didn't already provide requestContext // Only inject if the caller didn't already provide requestContext
// in the context object, to avoid clobbering custom test setups. // in the context object, to avoid clobbering custom test setups.
map.entry(Value::from("requestContext")).or_insert(rc_val); map.get_or_insert_with(Value::from("requestContext"), || rc_val);
} else if let Some(ref api_ver) = case.api_version { } else if let Some(ref api_ver) = case.api_version {
let map = ctx.as_object_mut()?; let map = ctx.as_object_mut()?;
if let std::collections::btree_map::Entry::Vacant(e) = if !map.contains_key(&Value::from("requestContext")) {
map.entry(Value::from("requestContext"))
{
let mut req_ctx = Value::new_object(); let mut req_ctx = Value::new_object();
let rc_map = req_ctx.as_object_mut()?; let rc_map = req_ctx.as_object_mut()?;
rc_map.insert(Value::from("apiVersion"), Value::from(api_ver.clone())); rc_map.insert(Value::from("apiVersion"), Value::from(api_ver.clone()));
e.insert(req_ctx); map.insert(Value::from("requestContext"), req_ctx);
} }
} }
+5 -1
View File
@@ -380,7 +380,11 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> {
} }
let path = Path::new(&path_str); let path = Path::new(&path_str);
let path_dir = path.strip_prefix(tests_path)?.parent().unwrap(); let path_dir = path.strip_prefix(tests_path)?.parent().unwrap();
let path_dir_str = path_dir.to_string_lossy().to_string(); // Normalize to forward slashes so the folder filter at the
// `folders.iter().any(|f| &path_dir_str == f)` check below matches
// CLI arguments like `v0/aggregates` on Windows, where
// `to_string_lossy` yields backslash separators by default.
let path_dir_str = path_dir.to_string_lossy().replace('\\', "/");
let folder_name = folder_name_from_path(path_dir); let folder_name = folder_name_from_path(path_dir);
let skip_rvm_for_folder = folder_name let skip_rvm_for_folder = folder_name
.as_deref() .as_deref()
@@ -137,3 +137,45 @@ cases:
- "Return { value: 7 }" # Return result set - "Return { value: 7 }" # Return result set
want_result: want_result:
set!: [1, null, 2] set!: [1, null, 2]
# Set iteration resumes from `Bound::Excluded(current_item)`, so the
# ComprehensionAdd path must snapshot the current value into
# `IterationState::Set.current_item` before advancing — otherwise
# iteration stops after the first element. This case exercises
# Set-source comprehension end-to-end to lock that requirement in.
- note: set_source_visits_all_elements
description: Comprehension over a Set source must yield every element, not just the first
example_rego: |
src := {1, 2, 3, 4}
{x | x := src[_]} # {1, 2, 3, 4}
literals:
- 1
- 2
- 3
- 4
instruction_params:
comprehension_start_params:
- mode: "Set"
collection_reg: 0
key_reg: 4
value_reg: 5
result_reg: 7
body_start: 11
comprehension_end: 13
instructions:
- "SetNew { dest: 0 }" # Build source set {1,2,3,4} in register 0
- "Load { dest: 1, literal_idx: 0 }"
- "SetAdd { set: 0, value: 1 }"
- "Load { dest: 2, literal_idx: 1 }"
- "SetAdd { set: 0, value: 2 }"
- "Load { dest: 3, literal_idx: 2 }"
- "SetAdd { set: 0, value: 3 }"
- "Load { dest: 6, literal_idx: 3 }"
- "SetAdd { set: 0, value: 6 }"
- "SetNew { dest: 7 }" # Initialize result set
- "ComprehensionStart { params_index: 0 }" # Iterate over Set source
- "ComprehensionAdd { value_reg: 5 }" # Add current value to result
- "Halt"
- "Return { value: 7 }"
want_result:
set!: [1, 2, 3, 4]