Compare commits

..

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
639bfe3246 Fix deep-review findings for prepare/closed-handle safety
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/c2c0f3ef-f641-4d83-94f8-a3cd3ede6db8

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-15 12:04:40 +00:00
copilot-swe-agent[bot]
4cc82e2fda Address review nits after validation feedback
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/f964122b-5af3-41a7-bc51-d87b7271a455

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 20:11:51 +00:00
copilot-swe-agent[bot]
196b6d68aa Fix target-aware prepare path and add regression test
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/f964122b-5af3-41a7-bc51-d87b7271a455

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 20:07:11 +00:00
copilot-swe-agent[bot]
c65e844f63 Strengthen Java clone test coverage
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/49ba4462-95d3-42c0-a302-db1b81df4f65

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 13:18:27 +00:00
copilot-swe-agent[bot]
730e6de75a Extend prepare API/docs across bindings
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/49ba4462-95d3-42c0-a302-db1b81df4f65

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-14 13:13:43 +00:00
copilot-swe-agent[bot]
72515f6d4c Expose wasm engine clone and add prepare API
Agent-Logs-Url: https://github.com/microsoft/regorus/sessions/58419b20-1586-4a23-85fb-5aed5f5d5961

Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
2026-05-13 20:16:02 +00:00
copilot-swe-agent[bot]
839933c933 Initial plan 2026-05-13 20:10:08 +00:00
110 changed files with 1534 additions and 5521 deletions

View File

@@ -8,5 +8,3 @@ steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 # full history needed for git diff against main fetch-depth: 0 # full history needed for git diff against main
- run: git fetch origin main:refs/remotes/origin/main
name: Ensure origin/main ref is available for diff computation

View File

@@ -25,21 +25,15 @@ Key constraints (details in copilot-instructions.md):
## Step 1: Get the Diff ## Step 1: Get the Diff
```bash ```bash
# Primary: use gh pr diff (works in cloud agent + any PR context). BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
# Fallback: git merge-base for local non-PR usage. || git merge-base origin/main HEAD 2>/dev/null)
if gh pr diff --name-only >/dev/null 2>&1; then if [ -z "$BASE" ]; then
echo "---STAT---" echo "ERROR: Cannot find upstream/main or origin/main. Cannot determine review scope."
gh pr diff --name-only exit 1
echo "---DIFF---"
gh pr diff
else
BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
|| git merge-base origin/main HEAD 2>/dev/null \
|| git merge-base main HEAD 2>/dev/null)
echo "Reviewing changes since: $BASE"
git diff "$BASE"..HEAD --stat
git diff "$BASE"..HEAD
fi fi
echo "Reviewing changes since: $BASE"
git diff "$BASE"..HEAD --stat
git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/'
``` ```
If the diff is empty, stop and report: "No changes found to review." If the diff is empty, stop and report: "No changes found to review."
@@ -202,9 +196,3 @@ one pass. If any were skipped, note them and briefly assess.
### Summary ### Summary
X findings (N critical, N high, N medium, N low). One sentence overall assessment. X findings (N critical, N high, N medium, N low). One sentence overall assessment.
### Output
After generating the report above, write the COMPLETE report to `/tmp/code-review-report.md`
using the `create` tool or shell. This ensures the full report is preserved even if
display output is truncated.

View File

@@ -40,25 +40,22 @@ Use `read_agent` with `wait: true` to wait for each background agent.
## Step 1: Get the Diff and Build Inventory ## Step 1: Get the Diff and Build Inventory
```bash ```bash
# Primary: use gh pr diff (works in cloud agent + any PR context). BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
# Fallback: git merge-base for local non-PR usage. || git merge-base origin/main HEAD 2>/dev/null)
if gh pr diff --name-only >/dev/null 2>&1; then if [ -z "$BASE" ]; then
echo "---STAT---" echo "ERROR: Cannot find upstream/main or origin/main."
gh pr diff --name-only exit 1
echo "---DIFF---"
gh pr diff
else
BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
|| git merge-base origin/main HEAD 2>/dev/null \
|| git merge-base main HEAD 2>/dev/null)
echo "Reviewing changes since: $BASE"
git diff "$BASE"..HEAD --stat
git diff "$BASE"..HEAD
fi fi
echo "Reviewing changes since: $BASE"
git diff "$BASE"..HEAD --stat
git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/' | head -2000
``` ```
If the diff is empty, stop and report: "No changes found to review." If the diff is empty, stop and report: "No changes found to review."
**Scope rule:** Focus on code files (`*.rs`, `*.toml`, examples). Do NOT pass
docs/config diffs to agents.
**Build a risk-classified inventory.** List every changed function, struct, **Build a risk-classified inventory.** List every changed function, struct,
impl, trait, pub item, and significant code block. Number them and tag with impl, trait, pub item, and significant code block. Number them and tag with
risk predicates: risk predicates:
@@ -109,10 +106,8 @@ Use `model: "gpt-5.4"` in the task tool call (provides model diversity).
> Get the diff: > Get the diff:
> ``` > ```
> BASE=$(git merge-base upstream/main HEAD 2>/dev/null \ > BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
> || git merge-base origin/main HEAD 2>/dev/null \ > || git merge-base origin/main HEAD 2>/dev/null)
> || git merge-base main HEAD 2>/dev/null) > git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/'
> # If no merge-base, use: gh pr diff
> git diff "$BASE"..HEAD # or: gh pr diff
> ``` > ```
> >
> Key regorus constraints: > Key regorus constraints:
@@ -166,10 +161,8 @@ Use `model: "claude-opus-4.6"` in the task tool call.
> Get the diff AND read full source files for context: > Get the diff AND read full source files for context:
> ``` > ```
> BASE=$(git merge-base upstream/main HEAD 2>/dev/null \ > BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
> || git merge-base origin/main HEAD 2>/dev/null \ > || git merge-base origin/main HEAD 2>/dev/null)
> || git merge-base main HEAD 2>/dev/null) > git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/'
> # If no merge-base, use: gh pr diff
> git diff "$BASE"..HEAD # or: gh pr diff
> ``` > ```
> Then use `view` to read the full source files that were changed. > Then use `view` to read the full source files that were changed.
> >
@@ -226,10 +219,8 @@ Use the default model (no `model` parameter).
> Get the diff: > Get the diff:
> ``` > ```
> BASE=$(git merge-base upstream/main HEAD 2>/dev/null \ > BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
> || git merge-base origin/main HEAD 2>/dev/null \ > || git merge-base origin/main HEAD 2>/dev/null)
> || git merge-base main HEAD 2>/dev/null) > git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/'
> # If no merge-base, use: gh pr diff
> git diff "$BASE"..HEAD # or: gh pr diff
> ``` > ```
> Use `view` to read surrounding context. > Use `view` to read surrounding context.
> >
@@ -448,10 +439,8 @@ Launch **1 general-purpose agent in background mode**.
> Get the diff: > Get the diff:
> ``` > ```
> BASE=$(git merge-base upstream/main HEAD 2>/dev/null \ > BASE=$(git merge-base upstream/main HEAD 2>/dev/null \
> || git merge-base origin/main HEAD 2>/dev/null \ > || git merge-base origin/main HEAD 2>/dev/null)
> || git merge-base main HEAD 2>/dev/null) > git diff "$BASE"..HEAD -- '*.rs' '*.toml' 'examples/'
> # If no merge-base, use: gh pr diff
> git diff "$BASE"..HEAD # or: gh pr diff
> ``` > ```
> Use `view` to read full source files. > Use `view` to read full source files.
> >
@@ -492,8 +481,8 @@ Launch **1 general-purpose agent in background mode**.
## Step 5: Synthesize and Report ## Step 5: Synthesize and Report
**CRITICAL:** Write the report to `/tmp/deep-review-report.md` FIRST, then display it. **IMPORTANT:** This is the primary output. Everything above was preparation.
Use a shell command to write the file before any other output in this step. Keep the report COMPACT — one finding per block, no filler prose.
Apply verdicts from the adversarial verifier: Apply verdicts from the adversarial verifier:
- **CONFIRMED**: keep at stated severity - **CONFIRMED**: keep at stated severity
@@ -533,9 +522,3 @@ would catch it. If not, name the minimal test that should exist.
X findings (N critical, N high, N medium, N low). Y "likely" findings. X findings (N critical, N high, N medium, N low). Y "likely" findings.
Z dropped (one-line reasons). Z dropped (one-line reasons).
Risk assessment in one sentence. Risk assessment in one sentence.
---
**Remember:** The report above MUST be written to `/tmp/deep-review-report.md` at the
START of Step 5 (before displaying it). Use shell: `cat > /tmp/deep-review-report.md << 'REPORT_EOF'`
... report content ... `REPORT_EOF`

View File

@@ -6,21 +6,6 @@ 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

106
Cargo.lock generated
View File

@@ -119,9 +119,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -162,9 +162,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -180,9 +180,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -402,9 +402,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.2.1" version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
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.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
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.16.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "email_address" name = "email_address"
@@ -650,9 +650,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
dependencies = [ dependencies = [
"foldhash 0.2.0", "foldhash 0.2.0",
] ]
@@ -835,7 +835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -869,9 +869,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -881,9 +881,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -944,9 +944,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -956,9 +956,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1353,9 +1353,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -1399,7 +1399,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cfg-if", "cfg-if",
@@ -1410,7 +1410,7 @@ dependencies = [
"dashmap", "dashmap",
"data-encoding", "data-encoding",
"globset", "globset",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"icu_casemap", "icu_casemap",
"indexmap", "indexmap",
"ipnet", "ipnet",
@@ -1431,7 +1431,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"spin 0.12.0", "spin 0.10.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.7" version = "2.2.6"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1518,9 +1518,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1580,9 +1580,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[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.12+spec-1.1.0" version = "0.25.11+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 = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@@ -1841,9 +1841,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1920,9 +1920,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -2029,9 +2029,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.3" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2173,18 +2173,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2 1.0.106", "proc-macro2 1.0.106",
"quote 1.0.45", "quote 1.0.45",
@@ -2193,9 +2193,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

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.1" version = "0.10.0"
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.150", default-features = false, features = ["alloc"] } serde_json = { version = "1.0.89", 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,14 +107,14 @@ 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.12.0", default-features = false, features = ["mutex", "spin_mutex"] } spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] }
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true } 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 }
semver = {version = "1.0.28", optional = true, default-features = false } semver = {version = "1.0.28", optional = true, default-features = false }
url = { version = "2.5.4", optional = true } url = { version = "2.5.4", optional = true }
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true } uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.46.5", default-features = false, optional = true } jsonschema = { version = "0.46.4", default-features = false, optional = true }
chrono = { version = "0.4.44", optional = true } chrono = { version = "0.4.44", optional = true }
chrono-tz = { version = "0.10.1", optional = true } chrono-tz = { version = "0.10.1", optional = true }
ipnet = { version = "2.12.0", optional = true, default-features = false } ipnet = { version = "2.12.0", 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.7", optional = true } mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.6", 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 }

View File

@@ -80,6 +80,10 @@ namespace regorus {
return std::unique_ptr<Engine>(new Engine(regorus_engine_clone(engine))); return std::unique_ptr<Engine>(new Engine(regorus_engine_clone(engine)));
} }
Result prepare() {
return Result(regorus_engine_prepare(engine));
}
Result set_rego_v0(bool enable) { Result set_rego_v0(bool enable) {
return Result(regorus_engine_set_rego_v0(engine, enable)); return Result(regorus_engine_set_rego_v0(engine, enable));
} }

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RegorusPackageVersion>0.10.1</RegorusPackageVersion> <RegorusPackageVersion>0.10.0</RegorusPackageVersion>
<RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix> <RegorusPackageVersionSuffix Condition="'$(VersionSuffix)' != ''">-$(VersionSuffix)</RegorusPackageVersionSuffix>
</PropertyGroup> </PropertyGroup>

View File

@@ -150,76 +150,3 @@ 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.

View File

@@ -43,28 +43,31 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Create_and_dispose_succeeds() public void Create_and_dispose_succeeds()
{ {
using var registry = AliasRegistry.Empty(); using var registry = new AliasRegistry();
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 = AliasRegistry.FromJson(AliasesJson); using var registry = new AliasRegistry();
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 = AliasRegistry.FromManifest(ManifestJson); using var registry = new AliasRegistry();
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 = AliasRegistry.FromJson(AliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -90,7 +93,8 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void NormalizeAndWrap_with_context_and_parameters() public void NormalizeAndWrap_with_context_and_parameters()
{ {
using var registry = AliasRegistry.FromJson(AliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -111,7 +115,8 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Denormalize_restores_properties() public void Denormalize_restores_properties()
{ {
using var registry = AliasRegistry.FromJson(AliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var normalized = @"{ var normalized = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -132,7 +137,8 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void Round_trip_normalize_then_denormalize() public void Round_trip_normalize_then_denormalize()
{ {
using var registry = AliasRegistry.FromJson(AliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(AliasesJson);
var resource = @"{ var resource = @"{
""name"": ""acct1"", ""name"": ""acct1"",
@@ -160,7 +166,8 @@ public class AliasRegistryTests
[TestMethod] [TestMethod]
public void DataPlane_manifest_normalize() public void DataPlane_manifest_normalize()
{ {
using var registry = AliasRegistry.FromManifest(ManifestJson); using var registry = new AliasRegistry();
registry.LoadManifest(ManifestJson);
var resource = @"{ var resource = @"{
""type"": ""Microsoft.KeyVault.Data/vaults/certificates"", ""type"": ""Microsoft.KeyVault.Data/vaults/certificates"",
@@ -178,7 +185,7 @@ public class AliasRegistryTests
[ExpectedException(typeof(InvalidOperationException))] [ExpectedException(typeof(InvalidOperationException))]
public void LoadJson_invalid_throws() public void LoadJson_invalid_throws()
{ {
using var builder = new AliasRegistryBuilder(); using var registry = new AliasRegistry();
builder.LoadJson("not valid json"); registry.LoadJson("not valid json");
} }
} }

View File

@@ -1,436 +0,0 @@
// 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");
}
}

View File

@@ -62,7 +62,8 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_produces_input_envelope() public void AliasRegistry_NormalizeAndWrap_produces_input_envelope()
{ {
using var registry = AliasRegistry.FromJson(StorageAliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap( var result = registry.NormalizeAndWrap(
StorageResourceJson, StorageResourceJson,
@@ -83,7 +84,8 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_flattens_properties() public void AliasRegistry_NormalizeAndWrap_flattens_properties()
{ {
using var registry = AliasRegistry.FromJson(StorageAliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap(StorageResourceJson); var result = registry.NormalizeAndWrap(StorageResourceJson);
Assert.IsNotNull(result); Assert.IsNotNull(result);
@@ -105,7 +107,8 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_preserves_type_field() public void AliasRegistry_NormalizeAndWrap_preserves_type_field()
{ {
using var registry = AliasRegistry.FromJson(StorageAliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(StorageAliasesJson);
var result = registry.NormalizeAndWrap(StorageResourceJson); var result = registry.NormalizeAndWrap(StorageResourceJson);
var doc = JsonNode.Parse(result!); var doc = JsonNode.Parse(result!);
@@ -122,7 +125,8 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_NormalizeAndWrap_includes_parameters() public void AliasRegistry_NormalizeAndWrap_includes_parameters()
{ {
using var registry = AliasRegistry.FromJson(StorageAliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(StorageAliasesJson);
var parametersJson = @"{ ""effect"": ""Deny"" }"; var parametersJson = @"{ ""effect"": ""Deny"" }";
var result = registry.NormalizeAndWrap( var result = registry.NormalizeAndWrap(
@@ -139,7 +143,8 @@ public class AzurePolicyTests
[TestMethod] [TestMethod]
public void AliasRegistry_Denormalize_roundtrips_correctly() public void AliasRegistry_Denormalize_roundtrips_correctly()
{ {
using var registry = AliasRegistry.FromJson(StorageAliasesJson); using var registry = new AliasRegistry();
registry.LoadJson(StorageAliasesJson);
// Normalize the ARM resource. // Normalize the ARM resource.
var envelope = registry.NormalizeAndWrap(StorageResourceJson); var envelope = registry.NormalizeAndWrap(StorageResourceJson);
@@ -172,7 +177,8 @@ public class AzurePolicyTests
} }
var aliasesJson = File.ReadAllText(aliasesPath); var aliasesJson = File.ReadAllText(aliasesPath);
using var registry = AliasRegistry.FromJson(aliasesJson); using var registry = new AliasRegistry();
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,

View File

@@ -8,43 +8,51 @@ using Regorus.Internal;
namespace Regorus namespace Regorus
{ {
/// <summary> /// <summary>
/// Immutable Azure Policy alias registry used for resource normalization /// Manages Azure Policy alias definitions used for resource normalization
/// and policy compilation. /// and policy compilation.
/// </summary> /// </summary>
public unsafe sealed class AliasRegistry : SafeHandleWrapper public unsafe sealed class AliasRegistry : SafeHandleWrapper
{ {
internal AliasRegistry(RegorusAliasRegistryHandle handle) /// <summary>
: base(handle, nameof(AliasRegistry)) /// Create an empty alias registry.
/// </summary>
public AliasRegistry()
: base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry))
{ {
} }
/// <summary> /// <summary>
/// Create an empty immutable alias registry. /// Load control-plane alias data (array of ProviderAliases) from a JSON string.
/// </summary> /// </summary>
public static AliasRegistry Empty() /// <param name="json">JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json)</param>
public void LoadJson(string json)
{ {
using var builder = new AliasRegistryBuilder(); Utf8Marshaller.WithUtf8(json, jsonPtr =>
return builder.Build(); {
UseHandle(regPtr =>
{
CheckAndDropResult(API.regorus_alias_registry_load_json(
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
return 0;
});
});
} }
/// <summary> /// <summary>
/// Create an immutable alias registry from control-plane alias JSON. /// Load a data-plane policy manifest from a JSON string.
/// </summary> /// </summary>
public static AliasRegistry FromJson(string json) /// <param name="json">JSON object containing a DataPolicyManifest</param>
public void LoadManifest(string json)
{ {
using var builder = new AliasRegistryBuilder(); Utf8Marshaller.WithUtf8(json, jsonPtr =>
builder.LoadJson(json); {
return builder.Build(); UseHandle(regPtr =>
} {
CheckAndDropResult(API.regorus_alias_registry_load_manifest(
/// <summary> (RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
/// Create an immutable alias registry from a data-plane manifest JSON document. return 0;
/// </summary> });
public static AliasRegistry FromManifest(string json) });
{
using var builder = new AliasRegistryBuilder();
builder.LoadManifest(json);
return builder.Build();
} }
/// <summary> /// <summary>
@@ -66,6 +74,11 @@ 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 =>
@@ -83,22 +96,27 @@ namespace Regorus
(byte*)ctxPtr, (byte*)paramsPtr)); (byte*)ctxPtr, (byte*)paramsPtr));
}); });
} }
else
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => {
UseHandle(regPtr => return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
{ UseHandle(regPtr =>
return ResultHelpers.GetStringResult( {
API.regorus_alias_registry_normalize_and_wrap( return ResultHelpers.GetStringResult(
(RegorusAliasRegistry*)regPtr, API.regorus_alias_registry_normalize_and_wrap(
(byte*)resPtr, (byte*)apiPtr, (RegorusAliasRegistry*)regPtr,
(byte*)ctxPtr, (byte*)paramsPtr)); (byte*)resPtr, (byte*)apiPtr,
})); (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 =>
@@ -113,16 +131,23 @@ namespace Regorus
(byte*)normPtr, null)); (byte*)normPtr, null));
}); });
} }
else
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => {
UseHandle(regPtr => return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
{ UseHandle(regPtr =>
return ResultHelpers.GetStringResult( {
API.regorus_alias_registry_denormalize( return ResultHelpers.GetStringResult(
(RegorusAliasRegistry*)regPtr, API.regorus_alias_registry_denormalize(
(byte*)normPtr, (byte*)apiPtr)); (RegorusAliasRegistry*)regPtr,
})); (byte*)normPtr, (byte*)apiPtr));
}));
}
}); });
} }
private static string? CheckAndDropResult(RegorusResult result)
{
return ResultHelpers.GetStringResult(result);
}
} }
} }

View File

@@ -1,69 +0,0 @@
// 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));
});
}
}
}

View File

@@ -1,183 +0,0 @@
// 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);
}
}
}
}

View File

@@ -68,6 +68,18 @@ namespace Regorus
}); });
} }
/// <summary>
/// Prepare internal evaluation structures without executing a query.
/// This is optional: if skipped, the first evaluation pays this setup cost.
/// </summary>
public void Prepare()
{
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_prepare((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public void SetStrictBuiltinErrors(bool strict) public void SetStrictBuiltinErrors(bool strict)
{ {
UseHandle(enginePtr => UseHandle(enginePtr =>

View File

@@ -92,6 +92,12 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine); internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine);
/// <summary>
/// Prepare a RegorusEngine for evaluation without executing a query.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_prepare", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_prepare(RegorusEngine* engine);
/// <summary> /// <summary>
/// Compile an RVM program from the engine state with entry points. /// Compile an RVM program from the engine state with entry points.
/// </summary> /// </summary>
@@ -178,14 +184,6 @@ 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>
@@ -498,20 +496,6 @@ 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
@@ -695,34 +679,10 @@ namespace Regorus.Internal
#region Alias Registry Methods #region Alias Registry Methods
/// <summary> /// <summary>
/// Create a new alias registry builder. /// Create a new, empty AliasRegistry.
/// </summary> /// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusAliasRegistryBuilder* regorus_alias_registry_builder_new(); internal static extern RegorusAliasRegistry* regorus_alias_registry_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.
@@ -730,6 +690,18 @@ 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>
@@ -957,14 +929,6 @@ 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>

View File

@@ -15,7 +15,7 @@ namespace Regorus
/// </summary> /// </summary>
public unsafe sealed class Program : SafeHandleWrapper public unsafe sealed class Program : SafeHandleWrapper
{ {
internal Program(RegorusProgramHandle handle) private Program(RegorusProgramHandle handle)
: base(handle, nameof(Program)) : base(handle, nameof(Program))
{ {
} }

View File

@@ -69,29 +69,5 @@ 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);
}
}
} }
} }

View File

@@ -106,24 +106,6 @@ 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>

View File

@@ -184,48 +184,28 @@ 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)

View File

@@ -232,9 +232,6 @@ 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)
@@ -495,80 +492,4 @@ 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");
}
} }

100
bindings/ffi/Cargo.lock generated
View File

@@ -98,9 +98,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -141,9 +141,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -153,9 +153,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "cbindgen" name = "cbindgen"
version = "0.29.3" version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c95537b45400390270fae69ac098d057c8f5399001cde9d04f700c105ddfff2d" checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799"
dependencies = [ dependencies = [
"clap", "clap",
"heck", "heck",
@@ -172,9 +172,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -285,9 +285,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.2.1" version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
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.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -508,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
dependencies = [ dependencies = [
"foldhash 0.2.0", "foldhash 0.2.0",
] ]
@@ -687,7 +687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -712,9 +712,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -724,9 +724,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -790,9 +790,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -802,9 +802,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -1082,9 +1082,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -1128,7 +1128,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1136,7 +1136,7 @@ dependencies = [
"dashmap", "dashmap",
"data-encoding", "data-encoding",
"globset", "globset",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"icu_casemap", "icu_casemap",
"indexmap", "indexmap",
"ipnet", "ipnet",
@@ -1163,7 +1163,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-ffi" name = "regorus-ffi"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cbindgen", "cbindgen",
@@ -1174,7 +1174,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.7" version = "2.2.6"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1255,9 +1255,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1314,9 +1314,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[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.3", "winnow 1.0.2",
] ]
[[package]] [[package]]
@@ -1535,9 +1535,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1688,9 +1688,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.3" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
@@ -1817,18 +1817,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1837,9 +1837,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -2,7 +2,7 @@
[package] [package]
name = "regorus-ffi" name = "regorus-ffi"
version = "0.10.1" version = "0.10.0"
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.150" serde_json = "1.0.140"
parking_lot = { version = "0.12", optional = true } parking_lot = { version = "0.12", optional = true }
[profile.release] [profile.release]

View File

@@ -5,108 +5,66 @@
#![cfg(feature = "azure_policy")] #![cfg(feature = "azure_policy")]
use crate::common::{from_c_str, to_ref, to_shared_ref, RegorusResult, RegorusStatus}; use crate::common::{from_c_str, to_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 alloc::sync::Arc; use anyhow::Result;
use anyhow::{anyhow, Result}; use core::ffi::c_char;
use core::ffi::{c_char, c_void}; use core::ptr;
use core::{mem, ptr};
use regorus::languages::azure_policy::aliases::AliasRegistry; use regorus::languages::azure_policy::aliases::AliasRegistry;
/// Mutable builder for `AliasRegistry`. /// Opaque wrapper for `AliasRegistry`.
///
/// This handle is intentionally single-threaded and must not be used
/// concurrently. Callers should finish loading alias data and then freeze it
/// into a `RegorusAliasRegistry` via `regorus_alias_registry_builder_build`.
pub struct RegorusAliasRegistryBuilder {
registry: AliasRegistry,
built: bool,
}
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 { pub struct RegorusAliasRegistry {
registry: Arc<AliasRegistry>, registry: 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 // Lifecycle
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Create a new, empty `AliasRegistry` builder. /// Create a new, empty `AliasRegistry`.
/// ///
/// The caller must eventually call `regorus_alias_registry_builder_drop`. /// The caller must eventually call `regorus_alias_registry_drop` to free the handle.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_builder_new() -> *mut RegorusAliasRegistryBuilder { pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry {
Box::into_raw(Box::new(RegorusAliasRegistryBuilder::new())) let wrapper = RegorusAliasRegistry {
registry: AliasRegistry::new(),
};
Box::into_raw(Box::new(wrapper))
} }
/// Drop a `RegorusAliasRegistryBuilder`. /// Drop a `RegorusAliasRegistry`.
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_alias_registry_builder_drop(builder: *mut RegorusAliasRegistryBuilder) { pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) {
if let Ok(builder) = to_ref(builder) { if let Ok(r) = to_ref(registry) {
unsafe { unsafe {
let _ = Box::from_raw(ptr::from_mut(builder)); let _ = Box::from_raw(ptr::from_mut(r));
} }
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Builder loading // Loading
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Load control-plane alias data (array of `ProviderAliases`) into the builder. /// Load control-plane alias data (array of `ProviderAliases`) into the registry.
/// ///
/// `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_builder_load_json( pub extern "C" fn regorus_alias_registry_load_json(
builder: *mut RegorusAliasRegistryBuilder, registry: *mut RegorusAliasRegistry,
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(builder)?.registry_mut()?.load_from_json(&json_str)?; to_ref(registry)?.registry.load_from_json(&json_str)?;
Ok(()) Ok(())
}(); }();
@@ -120,20 +78,20 @@ pub extern "C" fn regorus_alias_registry_builder_load_json(
}) })
} }
/// Load a data-plane policy manifest into the builder. /// Load a data-plane policy manifest into the registry.
/// ///
/// `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_builder_load_manifest( pub extern "C" fn regorus_alias_registry_load_manifest(
builder: *mut RegorusAliasRegistryBuilder, registry: *mut RegorusAliasRegistry,
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(builder)? to_ref(registry)?
.registry_mut()? .registry
.load_data_policy_manifest_json(&json_str)?; .load_data_policy_manifest_json(&json_str)?;
Ok(()) Ok(())
}(); }();
@@ -148,52 +106,16 @@ pub extern "C" fn regorus_alias_registry_builder_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}"))
}
}
})
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Frozen registry lifecycle // Queries
// ---------------------------------------------------------------------------
/// 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( pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult {
registry: *const RegorusAliasRegistry,
) -> RegorusResult {
with_unwind_guard(|| { with_unwind_guard(|| {
let output = || -> Result<i64> { let output = || -> Result<i64> {
let len = to_shared_ref(registry)?.registry.len(); let len = to_ref(registry)?.registry.len();
Ok(len as i64) Ok(len as i64)
}(); }();
@@ -212,9 +134,15 @@ pub extern "C" fn regorus_alias_registry_len(
/// ///
/// 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: *const RegorusAliasRegistry, registry: *mut 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,
@@ -240,7 +168,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_shared_ref(registry)?.registry.normalize_and_wrap( let wrapped = to_ref(registry)?.registry.normalize_and_wrap(
&resource, &resource,
api_ver.as_deref(), api_ver.as_deref(),
Some(context), Some(context),
@@ -257,9 +185,14 @@ 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: *const RegorusAliasRegistry, registry: *mut RegorusAliasRegistry,
normalized_json: *const c_char, normalized_json: *const c_char,
api_version: *const c_char, api_version: *const c_char,
) -> RegorusResult { ) -> RegorusResult {
@@ -279,7 +212,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_shared_ref(registry)? let result = to_ref(registry)?
.registry .registry
.denormalize(&normalized, api_ver.as_deref()); .denormalize(&normalized, api_ver.as_deref());
result.to_json_str() result.to_json_str()
@@ -299,10 +232,12 @@ 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");
@@ -313,51 +248,12 @@ 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": [{
@@ -383,21 +279,20 @@ mod tests {
}"#; }"#;
#[test] #[test]
fn lifecycle_builder_build_and_drop() { fn lifecycle_new_and_drop() {
let builder = regorus_alias_registry_builder_new(); let reg = regorus_alias_registry_new();
assert!(!builder.is_null()); assert!(!reg.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 = build_registry_with_json(ALIASES); let reg = regorus_alias_registry_new();
let json = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_len(reg); let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1); assert_eq!(assert_ok_int(&r), 1);
@@ -408,7 +303,12 @@ mod tests {
#[test] #[test]
fn load_manifest_and_check_len() { fn load_manifest_and_check_len() {
let reg = build_registry_with_manifest(MANIFEST); let reg = regorus_alias_registry_new();
let json = c(MANIFEST);
let r = regorus_alias_registry_load_manifest(reg, json.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let r = regorus_alias_registry_len(reg); let r = regorus_alias_registry_len(reg);
assert_eq!(assert_ok_int(&r), 1); assert_eq!(assert_ok_int(&r), 1);
@@ -419,39 +319,23 @@ mod tests {
#[test] #[test]
fn load_invalid_json_returns_error() { fn load_invalid_json_returns_error() {
let builder = regorus_alias_registry_builder_new(); let reg = regorus_alias_registry_new();
let bad = c("not valid json"); let bad = c("not valid json");
let r = regorus_alias_registry_builder_load_json(builder, bad.as_ptr()); let r = regorus_alias_registry_load_json(reg, 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_builder_drop(builder); regorus_alias_registry_drop(reg);
}
#[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 = build_registry_with_json(ALIASES); let reg = regorus_alias_registry_new();
let aliases = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let resource = c(r#"{ let resource = c(r#"{
"name": "acct1", "name": "acct1",
@@ -462,6 +346,7 @@ 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(),
@@ -472,6 +357,7 @@ 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!(
@@ -487,13 +373,16 @@ 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);
@@ -503,6 +392,7 @@ 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"
@@ -513,7 +403,11 @@ mod tests {
#[test] #[test]
fn denormalize_invalid_json_returns_error() { fn denormalize_invalid_json_returns_error() {
let reg = build_registry_with_json(ALIASES); let reg = regorus_alias_registry_new();
let aliases = c(ALIASES);
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let bad = c("not json"); let bad = c("not json");
let api = c("2023-01-01"); let api = c("2023-01-01");
@@ -526,7 +420,11 @@ mod tests {
#[test] #[test]
fn normalize_data_plane_manifest() { fn normalize_data_plane_manifest() {
let reg = build_registry_with_manifest(MANIFEST); let reg = regorus_alias_registry_new();
let manifest = c(MANIFEST);
let r = regorus_alias_registry_load_manifest(reg, manifest.as_ptr());
assert_eq!(r.status, RegorusStatus::Ok);
regorus_result_drop(r);
let resource = c(r#"{ let resource = c(r#"{
"type": "Microsoft.KeyVault.Data/vaults/certificates", "type": "Microsoft.KeyVault.Data/vaults/certificates",
@@ -555,12 +453,7 @@ mod tests {
#[test] #[test]
fn empty_registry_normalize() { fn empty_registry_normalize() {
let builder = regorus_alias_registry_builder_new(); let reg = regorus_alias_registry_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("{}");
@@ -577,6 +470,7 @@ 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");

View File

@@ -236,10 +236,6 @@ 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(),

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, to_shared_ref, RegorusResult, RegorusStatus}; use crate::common::{from_c_str, 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,220 +208,6 @@ 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);
@@ -429,402 +215,3 @@ 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);
}
}
}

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_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? let result = to_ref(compiled_policy)?
.compiled_policy .compiled_policy
.eval_with_input(input_value)?; .eval_with_input(input_value)?;
result.to_json_str() result.to_json_str()
@@ -65,9 +65,7 @@ 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_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?;
.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))
}(); }();

View File

@@ -2,8 +2,7 @@
// 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, to_shared_ref, RegorusResult, from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus,
RegorusStatus,
}; };
use crate::compiled_policy::RegorusCompiledPolicy; use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig; use crate::limits::RegorusExecutionTimerConfig;
@@ -194,12 +193,27 @@ 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_shared_ref(engine as *const RegorusEngine) { match to_ref(engine) {
Ok(e) => Box::into_raw(Box::new(e.clone())), Ok(e) => Box::into_raw(Box::new(e.clone())),
_ => ptr::null_mut(), _ => ptr::null_mut(),
} }
} }
/// Prepare a [`RegorusEngine`] for evaluation without executing a query.
///
/// This is optional. If not called, first eval performs the same setup.
/// If policy/data changes after preparation, setup is invalidated.
#[no_mangle]
pub extern "C" fn regorus_engine_prepare(engine: *mut RegorusEngine) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.prepare()
}())
})
}
#[no_mangle] #[no_mangle]
pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) { pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) {
if let Ok(e) = to_ref(engine) { if let Ok(e) = to_ref(engine) {
@@ -224,7 +238,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?)
}()) }())
@@ -239,7 +253,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?)
}()) }())
@@ -257,7 +271,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?)?)
}()) }())
@@ -271,7 +285,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)
}()) }())
@@ -285,7 +299,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_policies_as_json() guard.get_policies_as_json()
}()) }())
@@ -300,7 +314,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?)?)
}()) }())
@@ -314,7 +328,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_data(); guard.clear_data();
Ok(()) Ok(())
@@ -333,7 +347,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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(())
@@ -349,7 +363,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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(())
@@ -368,7 +382,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?)
@@ -391,7 +405,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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()
}(); }();
@@ -414,7 +428,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_enable_coverage(enable); guard.set_enable_coverage(enable);
Ok(()) Ok(())
@@ -430,7 +444,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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()?)?)
}(); }();
@@ -452,7 +466,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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(())
@@ -466,20 +480,18 @@ pub extern "C" fn regorus_engine_set_execution_timer_config(
engine: *mut RegorusEngine, engine: *mut RegorusEngine,
config: *const RegorusExecutionTimerConfig, config: *const RegorusExecutionTimerConfig,
) -> RegorusResult { ) -> RegorusResult {
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 config = unsafe {
let config = unsafe { config
config .as_ref()
.as_ref() .copied()
.copied() .ok_or_else(|| anyhow!("execution timer config pointer is null"))?
.ok_or_else(|| anyhow!("execution timer config pointer is null"))? };
}; let mut guard = engine.try_write()?;
let mut guard = engine.try_write()?; guard.set_execution_timer_config(config.to_execution_timer_config()?);
guard.set_execution_timer_config(config.to_execution_timer_config()?); Ok(())
Ok(()) }())
}())
})
} }
#[no_mangle] #[no_mangle]
@@ -487,14 +499,12 @@ 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 {
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_execution_timer_config();
guard.clear_execution_timer_config(); Ok(())
Ok(()) }())
}())
})
} }
/// Set the policy length limits used when loading policies. /// Set the policy length limits used when loading policies.
@@ -505,7 +515,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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(())
@@ -520,7 +530,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_policy_length_config(); guard.clear_policy_length_config();
Ok(()) Ok(())
@@ -538,7 +548,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_coverage_report()?.to_string_pretty() guard.get_coverage_report()?.to_string_pretty()
}(); }();
@@ -557,7 +567,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.clear_coverage_data(); guard.clear_coverage_data();
Ok(()) Ok(())
@@ -576,7 +586,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_gather_prints(enable); guard.set_gather_prints(enable);
Ok(()) Ok(())
@@ -591,7 +601,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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()?)?)
}(); }();
@@ -610,7 +620,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let guard = engine.try_read()?; let guard = engine.try_read()?;
guard.get_ast_as_json() guard.get_ast_as_json()
}(); }();
@@ -631,7 +641,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)
@@ -653,7 +663,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)
@@ -675,7 +685,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
let mut guard = engine.try_write()?; let mut guard = engine.try_write()?;
guard.set_rego_v0(enable); guard.set_rego_v0(enable);
Ok(()) Ok(())
@@ -697,7 +707,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_shared_ref(engine as *const RegorusEngine) { let engine = match to_ref(engine) {
Ok(engine) => engine, Ok(engine) => engine,
Err(e) => { Err(e) => {
return RegorusResult::err_with_message( return RegorusResult::err_with_message(
@@ -746,7 +756,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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 })
@@ -805,7 +815,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_shared_ref(engine as *const RegorusEngine)?; let engine = to_ref(engine)?;
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)?;

View File

@@ -2,8 +2,7 @@
// 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_shared_ref, RegorusBuffer, RegorusResult, from_c_str, to_ref, to_regorus_result, RegorusBuffer, RegorusResult, RegorusStatus,
RegorusStatus,
}; };
use crate::compile::RegorusPolicyModule; use crate::compile::RegorusPolicyModule;
use crate::compiled_policy::RegorusCompiledPolicy; use crate::compiled_policy::RegorusCompiledPolicy;
@@ -107,8 +106,7 @@ 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 = let compiled_policy = &to_ref(compiled_policy)?.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 })))
}(); }();
@@ -189,7 +187,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_shared_ref(program as *const RegorusProgram)?.program; let program = &to_ref(program)?.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))
}(); }();
@@ -213,10 +211,7 @@ 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() { if data.is_null() && len > 0 {
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) };
@@ -254,7 +249,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_shared_ref(program as *const RegorusProgram)?.program; let program = &to_ref(program)?.program;
Ok(generate_assembly_listing( Ok(generate_assembly_listing(
program, program,
&AssemblyListingConfig::default(), &AssemblyListingConfig::default(),
@@ -275,7 +270,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_shared_ref(program as *const RegorusProgram)?.program; let program = &to_ref(program)?.program;
Ok(generate_tabular_assembly_listing( Ok(generate_tabular_assembly_listing(
program, program,
&AssemblyListingConfig::default(), &AssemblyListingConfig::default(),
@@ -302,9 +297,7 @@ 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_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? let policy = to_ref(compiled_policy)?.compiled_policy.clone();
.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),
)))) ))))
@@ -325,11 +318,9 @@ 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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
let program = to_shared_ref(program as *const RegorusProgram)? let program = to_ref(program)?.program.clone();
.program
.clone();
guard.load_program(program); guard.load_program(program);
Ok(()) Ok(())
}()) }())
@@ -341,7 +332,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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)?;
@@ -358,7 +349,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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);
@@ -367,33 +358,6 @@ 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(
@@ -402,7 +366,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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(())
@@ -418,7 +382,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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(())
@@ -431,7 +395,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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,
@@ -449,7 +413,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
let mut guard = vm.try_write()?; let mut guard = vm.try_write()?;
guard.set_step_mode(enabled); guard.set_step_mode(enabled);
Ok(()) Ok(())
@@ -466,7 +430,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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()?));
@@ -483,7 +447,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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()
@@ -504,7 +468,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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)?;
@@ -526,7 +490,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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()
@@ -548,7 +512,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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)?)?)
@@ -571,7 +535,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_shared_ref(vm as *const RegorusRvm)?; let vm = to_ref(vm)?;
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))

View File

@@ -28,6 +28,17 @@ func (e *Engine) Clone() *Engine {
return c return c
} }
func (e *Engine) Prepare() error {
result := C.regorus_engine_prepare(e.e)
defer C.regorus_result_drop(result)
if result.status != C.Ok {
return fmt.Errorf("%s", C.GoString(result.error_message))
}
return nil
}
func (e *Engine) SetRegoV0(enable bool) error { func (e *Engine) SetRegoV0(enable bool) error {
result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable)) result := C.regorus_engine_set_rego_v0(e.e, C.bool(enable))
defer C.regorus_result_drop(result) defer C.regorus_result_drop(result)

View File

@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -109,9 +109,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -199,9 +199,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -380,9 +380,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -530,7 +530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -598,9 +598,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -610,9 +610,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -670,9 +670,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -682,9 +682,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -954,9 +954,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -1000,7 +1000,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1032,7 +1032,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-java" name = "regorus-java"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"jni", "jni",
@@ -1042,7 +1042,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.7" version = "2.2.6"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1128,9 +1128,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1194,9 +1194,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1639,18 +1639,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1659,9 +1659,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -2,7 +2,7 @@
[package] [package]
name = "regorus-java" name = "regorus-java"
version = "0.10.1" version = "0.10.0"
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.150" serde_json = "1.0.112"
jni = "0.22.4" jni = "0.22.4"
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }

View File

@@ -23,6 +23,14 @@ JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeNewEngine
JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_nativeClone
(JNIEnv *, jclass, jlong); (JNIEnv *, jclass, jlong);
/*
* Class: com_microsoft_regorus_Engine
* Method: nativePrepare
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_nativePrepare
(JNIEnv *, jclass, jlong);
/* /*
* Class: com_microsoft_regorus_Engine * Class: com_microsoft_regorus_Engine
* Method: nativeAddPolicy * Method: nativeAddPolicy

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.1</version> <version>0.10.0</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>

View File

@@ -27,13 +27,30 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone( pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
_env: EnvUnowned, env: EnvUnowned,
_class: JClass, _class: JClass,
engine_ptr: jlong, engine_ptr: jlong,
) -> jlong { ) -> jlong {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; let res = throw_err(env, |_env| {
let c = engine.clone(); let engine = unsafe { &mut *get_engine_ptr(engine_ptr)? };
Box::into_raw(Box::new(c)) as jlong let c = engine.clone();
Ok(Box::into_raw(Box::new(c)) as jlong)
});
res.unwrap_or_default()
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativePrepare(
env: EnvUnowned,
_class: JClass,
engine_ptr: jlong,
) {
let _ = throw_err(env, |_env| {
let engine = unsafe { &mut *get_engine_ptr(engine_ptr)? };
engine.prepare()?;
Ok(())
});
} }
#[no_mangle] #[no_mangle]
@@ -437,6 +454,9 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
_class: JClass, _class: JClass,
engine_ptr: jlong, engine_ptr: jlong,
) { ) {
if engine_ptr == 0 {
return;
}
unsafe { unsafe {
let _engine = Box::from_raw(engine_ptr as *mut Engine); let _engine = Box::from_raw(engine_ptr as *mut Engine);
} }
@@ -462,7 +482,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) { for (id, content) in ids.into_iter().zip(contents.into_iter()) {
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()),
@@ -816,6 +836,13 @@ fn throw_err<T>(mut env: EnvUnowned, f: impl FnOnce(&mut Env) -> Result<T>) -> R
} }
} }
fn get_engine_ptr(engine_ptr: jlong) -> Result<*mut Engine> {
if engine_ptr == 0 {
return Err(anyhow::anyhow!("Engine is closed"));
}
Ok(engine_ptr as *mut Engine)
}
fn get_string_array(env: &mut Env, array: jobjectArray) -> Result<Vec<String>> { fn get_string_array(env: &mut Env, array: jobjectArray) -> Result<Vec<String>> {
if array.is_null() { if array.is_null() {
return Ok(Vec::new()); return Ok(Vec::new());

View File

@@ -21,6 +21,7 @@ public class Engine implements AutoCloseable, Cloneable {
// if you update the native API. // if you update the native API.
private static native long nativeNewEngine(); private static native long nativeNewEngine();
private static native long nativeClone(long enginePtr); private static native long nativeClone(long enginePtr);
private static native void nativePrepare(long enginePtr);
private static native void nativeSetRegoV0(long enginePtr, boolean enable); private static native void nativeSetRegoV0(long enginePtr, boolean enable);
private static native String nativeAddPolicy(long enginePtr, String path, String rego); private static native String nativeAddPolicy(long enginePtr, String path, String rego);
private static native String nativeAddPolicyFromFile(long enginePtr, String path); private static native String nativeAddPolicyFromFile(long enginePtr, String path);
@@ -45,7 +46,7 @@ public class Engine implements AutoCloseable, Cloneable {
// Pointer to Engine allocated on Rust's heap, all native methods works on // Pointer to Engine allocated on Rust's heap, all native methods works on
// engine expects this pointer. It is free'd in `close` method. // engine expects this pointer. It is free'd in `close` method.
private final long enginePtr; private long enginePtr;
/** /**
* Creates a new Regorus Engine. * Creates a new Regorus Engine.
@@ -63,7 +64,15 @@ public class Engine implements AutoCloseable, Cloneable {
* Efficiently clones an Engine. * Efficiently clones an Engine.
*/ */
public Engine clone() { public Engine clone() {
return new Engine(nativeClone(enginePtr)); return new Engine(nativeClone(requireOpen()));
}
/**
* Prepares internal evaluation structures without executing a query.
* Optional: if skipped, first evaluation performs the same setup.
*/
public void prepare() {
nativePrepare(requireOpen());
} }
/** /**
@@ -73,7 +82,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setRegoV0(boolean enable) { public void setRegoV0(boolean enable) {
nativeSetRegoV0(enginePtr, enable); nativeSetRegoV0(requireOpen(), enable);
} }
/** /**
@@ -85,7 +94,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Rego package defined in the policy. * @return Rego package defined in the policy.
*/ */
public String addPolicy(String filename, String rego) { public String addPolicy(String filename, String rego) {
return nativeAddPolicy(enginePtr, filename, rego); return nativeAddPolicy(requireOpen(), filename, rego);
} }
/** /**
@@ -96,7 +105,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Rego package defined in the policy. * @return Rego package defined in the policy.
*/ */
public String addPolicyFromFile(String path) { public String addPolicyFromFile(String path) {
return nativeAddPolicyFromFile(enginePtr, path); return nativeAddPolicyFromFile(requireOpen(), path);
} }
/** /**
@@ -105,7 +114,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return List of Rego packages as a JSON array of strings. * @return List of Rego packages as a JSON array of strings.
*/ */
public String getPackages() { public String getPackages() {
return nativeGetPackages(enginePtr); return nativeGetPackages(requireOpen());
} }
/** /**
@@ -114,14 +123,14 @@ public class Engine implements AutoCloseable, Cloneable {
* @return List of Rego policies as a JSON array of sources. * @return List of Rego policies as a JSON array of sources.
*/ */
public String getPolicies() { public String getPolicies() {
return nativeGetPolicies(enginePtr); return nativeGetPolicies(requireOpen());
} }
/** /**
* Clears the data document. * Clears the data document.
*/ */
public void clearData() { public void clearData() {
nativeClearData(enginePtr); nativeClearData(requireOpen());
} }
/** /**
@@ -143,7 +152,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param data Inline data document. * @param data Inline data document.
*/ */
public void addDataJson(String data) throws RuntimeException { public void addDataJson(String data) throws RuntimeException {
nativeAddDataJson(enginePtr, data); nativeAddDataJson(requireOpen(), data);
} }
/** /**
@@ -160,7 +169,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param path Path to JSON data document. * @param path Path to JSON data document.
*/ */
public void addDataJsonFromFile(String path) throws RuntimeException { public void addDataJsonFromFile(String path) throws RuntimeException {
nativeAddDataJsonFromFile(enginePtr, path); nativeAddDataJsonFromFile(requireOpen(), path);
} }
/** /**
@@ -169,7 +178,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param input inline JSON input. * @param input inline JSON input.
*/ */
public void setInputJson(String input) { public void setInputJson(String input) {
nativeSetInputJson(enginePtr, input); nativeSetInputJson(requireOpen(), input);
} }
/** /**
@@ -178,7 +187,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @param path Path to JSON input. * @param path Path to JSON input.
*/ */
public void setInputJsonFromFile(String path) { public void setInputJsonFromFile(String path) {
nativeSetInputJsonFromFile(enginePtr, path); nativeSetInputJsonFromFile(requireOpen(), path);
} }
/** /**
@@ -189,7 +198,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Query results as a JSON string. * @return Query results as a JSON string.
*/ */
public String evalQuery(String query) { public String evalQuery(String query) {
return nativeEvalQuery(enginePtr, query); return nativeEvalQuery(requireOpen(), query);
} }
/** /**
@@ -200,7 +209,7 @@ public class Engine implements AutoCloseable, Cloneable {
* @return Value of the rule as a JSON string. * @return Value of the rule as a JSON string.
*/ */
public String evalRule(String rule) { public String evalRule(String rule) {
return nativeEvalRule(enginePtr, rule); return nativeEvalRule(requireOpen(), rule);
} }
/** /**
@@ -210,7 +219,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setEnableCoverage(boolean enable) { public void setEnableCoverage(boolean enable) {
nativeSetEnableCoverage(enginePtr, enable); nativeSetEnableCoverage(requireOpen(), enable);
} }
/** /**
@@ -218,7 +227,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void clearCoverageData() { public void clearCoverageData() {
nativeClearCoverageData(enginePtr); nativeClearCoverageData(requireOpen());
} }
/** /**
@@ -228,7 +237,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String getCoverageReport() { public String getCoverageReport() {
return nativeGetCoverageReport(enginePtr); return nativeGetCoverageReport(requireOpen());
} }
/** /**
@@ -238,7 +247,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String getCoverageReportPretty() { public String getCoverageReportPretty() {
return nativeGetCoverageReportPretty(enginePtr); return nativeGetCoverageReportPretty(requireOpen());
} }
/** /**
@@ -248,7 +257,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public void setGatherPrints(boolean b) { public void setGatherPrints(boolean b) {
nativeSetGatherPrints(enginePtr, b); nativeSetGatherPrints(requireOpen(), b);
} }
/** /**
@@ -258,7 +267,7 @@ public class Engine implements AutoCloseable, Cloneable {
* *
*/ */
public String takePrints() { public String takePrints() {
return nativeTakePrints(enginePtr); return nativeTakePrints(requireOpen());
} }
/** /**
@@ -267,24 +276,34 @@ public class Engine implements AutoCloseable, Cloneable {
* @param config Policy length configuration. * @param config Policy length configuration.
*/ */
public void setPolicyLengthConfig(PolicyLengthConfig config) { public void setPolicyLengthConfig(PolicyLengthConfig config) {
nativeSetPolicyLengthConfig(enginePtr, config.maxCol, config.maxFileBytes, config.maxLines); nativeSetPolicyLengthConfig(requireOpen(), config.maxCol, config.maxFileBytes, config.maxLines);
} }
/** /**
* Clear the policy length configuration, reverting to defaults. * Clear the policy length configuration, reverting to defaults.
*/ */
public void clearPolicyLengthConfig() { public void clearPolicyLengthConfig() {
nativeClearPolicyLengthConfig(enginePtr); nativeClearPolicyLengthConfig(requireOpen());
} }
long getPtr() { long getPtr() {
return requireOpen();
}
private long requireOpen() {
if (enginePtr == 0) {
throw new IllegalStateException("Engine is closed");
}
return enginePtr; return enginePtr;
} }
@Override @Override
public void close() { public void close() {
nativeDestroyEngine(enginePtr); if (enginePtr != 0) {
nativeDestroyEngine(enginePtr);
enginePtr = 0;
}
} }
// Loading native library from JAR is adapted from: // Loading native library from JAR is adapted from:

View File

@@ -22,8 +22,19 @@ public class EngineTest extends TestCase
"package test\nmessage = concat(\", \", [input.message, data.message])" "package test\nmessage = concat(\", \", [input.message, data.message])"
); );
engine.addDataJson("{\"message\":\"World!\"}"); engine.addDataJson("{\"message\":\"World!\"}");
engine.prepare();
engine.setInputJson("{\"message\":\"Hello\"}"); engine.setInputJson("{\"message\":\"Hello\"}");
resJson = engine.evalQuery("data.test.message"); resJson = engine.evalQuery("data.test.message");
try (Engine template = engine.clone()) {
template.setInputJson("{\"message\":\"Hi\"}");
String templateResJson = template.evalQuery("data.test.message");
Map templateRes = new Gson().fromJson(templateResJson, Map.class);
ArrayList templateResults = (ArrayList) templateRes.get("result");
ArrayList templateExpressions = (ArrayList) ((Map) templateResults.get(0)).get("expressions");
Map templateExpression = (Map) templateExpressions.get(0);
Assert.assertEquals("Hi, World!", templateExpression.get("value"));
}
} }
Gson gson = new Gson(); Gson gson = new Gson();
@@ -33,4 +44,28 @@ public class EngineTest extends TestCase
Map expression = (Map) expressions.get(0); Map expression = (Map) expressions.get(0);
Assert.assertEquals("Hello, World!", expression.get("value")); Assert.assertEquals("Hello, World!", expression.get("value"));
} }
public void test_closed_engine_operations_throw()
{
Engine engine = new Engine();
engine.close();
try {
engine.prepare();
fail("prepare should fail on closed engine");
} catch (IllegalStateException expected) {
}
try {
engine.clone();
fail("clone should fail on closed engine");
} catch (IllegalStateException expected) {
}
try {
engine.evalQuery("data");
fail("evalQuery should fail on closed engine");
} catch (IllegalStateException expected) {
}
}
} }

View File

@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -91,9 +91,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -183,9 +183,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -364,9 +364,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -514,7 +514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -533,9 +533,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -545,9 +545,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -605,9 +605,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -617,9 +617,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -837,9 +837,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.29.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"libc", "libc",
@@ -852,18 +852,18 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.29.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
dependencies = [ dependencies = [
"target-lexicon", "target-lexicon",
] ]
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.29.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
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.29.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@@ -883,12 +883,13 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.29.0" version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"pyo3-build-config",
"quote", "quote",
"syn", "syn",
] ]
@@ -962,9 +963,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -1008,7 +1009,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1040,7 +1041,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.7" version = "2.2.6"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1054,7 +1055,7 @@ dependencies = [
[[package]] [[package]]
name = "regoruspy" name = "regoruspy"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ordered-float", "ordered-float",
@@ -1119,9 +1120,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1169,9 +1170,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
@@ -1331,9 +1332,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -1344,9 +1345,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -1354,9 +1355,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -1367,9 +1368,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1592,18 +1593,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1612,9 +1613,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -2,7 +2,7 @@
[package] [package]
name = "regoruspy" name = "regoruspy"
version = "0.10.1" version = "0.10.0"
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.29.0", features = ["abi3-py310", "anyhow", "extension-module"] } pyo3 = { version = "0.28.3", 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.150" serde_json = "1.0.140"

View File

@@ -463,6 +463,13 @@ impl Engine {
self.engine.take_prints() self.engine.take_prints()
} }
/// Prepare internal evaluation structures without executing a query.
///
/// Optional: if skipped, first evaluation performs the same setup.
pub fn prepare(&mut self) -> Result<()> {
self.engine.prepare()
}
/// Clone a [`Engine`] /// Clone a [`Engine`]
/// ///
/// To avoid having to parse same policy again, the engine can be cloned /// To avoid having to parse same policy again, the engine can be cloned

View File

@@ -87,6 +87,7 @@ report = engine.get_coverage_report_pretty()
print(report) print(report)
# Clone engine # Clone engine
engine.prepare()
engine1 = engine.clone() engine1 = engine.clone()

View File

@@ -48,9 +48,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bindgen" name = "bindgen"
@@ -109,9 +109,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -121,9 +121,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -212,9 +212,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -223,9 +223,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.16.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "email_address" name = "email_address"
@@ -393,9 +393,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -543,7 +543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -571,9 +571,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -583,9 +583,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -653,9 +653,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -688,9 +688,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -994,9 +994,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -1040,7 +1040,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1071,7 +1071,7 @@ dependencies = [
[[package]] [[package]]
name = "regorus-mimalloc" name = "regorus-mimalloc"
version = "2.2.7" version = "2.2.6"
dependencies = [ dependencies = [
"regorus-mimalloc-sys", "regorus-mimalloc-sys",
] ]
@@ -1085,7 +1085,7 @@ dependencies = [
[[package]] [[package]]
name = "regorusrb" name = "regorusrb"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"magnus", "magnus",
"regorus", "regorus",
@@ -1162,9 +1162,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1229,9 +1229,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1652,18 +1652,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1672,9 +1672,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "regorusrb" name = "regorusrb"
version = "0.10.1" version = "0.10.0"
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"

View File

@@ -115,6 +115,13 @@ impl Engine {
Ok(()) Ok(())
} }
fn prepare(&self) -> Result<(), Error> {
self.engine
.borrow_mut()
.prepare()
.map_err(|e| Error::new(runtime_error(), format!("Failed to prepare engine: {e}")))
}
fn get_packages(&self) -> Result<Vec<String>, Error> { fn get_packages(&self) -> Result<Vec<String>, Error> {
self.engine self.engine
.borrow() .borrow()
@@ -373,6 +380,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
method!(Engine::add_data_from_json_file, 1), method!(Engine::add_data_from_json_file, 1),
)?; )?;
engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?; engine_class.define_method("clear_data", method!(Engine::clear_data, 0))?;
engine_class.define_method("prepare", method!(Engine::prepare, 0))?;
// input operations // input operations
engine_class.define_method("set_input", method!(Engine::set_input, 1))?; engine_class.define_method("set_input", method!(Engine::set_input, 1))?;

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Regorus module Regorus
VERSION = "0.10.1" VERSION = "0.10.0"
end end

View File

@@ -150,6 +150,7 @@ class TestRegorus < Minitest::Test
end end
def test_engine_cloning def test_engine_cloning
@engine.prepare
cloned_engine = @engine.clone cloned_engine = @engine.clone
assert_instance_of ::Regorus::Engine, cloned_engine assert_instance_of ::Regorus::Engine, cloned_engine

View File

@@ -59,9 +59,9 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
@@ -102,9 +102,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
@@ -120,9 +120,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.62" version = "1.2.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -200,9 +200,9 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.6" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -396,9 +396,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -546,7 +546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -565,9 +565,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.99" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -577,9 +577,9 @@ dependencies = [
[[package]] [[package]]
name = "jsonschema" name = "jsonschema"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5fe5206f06e589caf25e79fc05ccdf91fca745685fe9fe1a13bbdfb479a631" checksum = "fc59d2432e047d6090ba1d83c782d0128bd6203857978218f5614dbd3287281f"
dependencies = [ dependencies = [
"ahash", "ahash",
"bytecount", "bytecount",
@@ -643,9 +643,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.30" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -655,9 +655,9 @@ checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.1" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "micromap" name = "micromap"
@@ -953,9 +953,9 @@ dependencies = [
[[package]] [[package]]
name = "referencing" name = "referencing"
version = "0.46.5" version = "0.46.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e4e17ef386c5383591d07623d3de49cbc601156e7582973e6db98d66a57de2" checksum = "cb674900ca31acd75c4aaf63f48e43e719631c0539ea5a9e64163d1296bcb730"
dependencies = [ dependencies = [
"ahash", "ahash",
"fluent-uri", "fluent-uri",
@@ -999,7 +999,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "regorus" name = "regorus"
version = "0.10.1" version = "0.10.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1030,7 +1030,7 @@ dependencies = [
[[package]] [[package]]
name = "regorusjs" name = "regorusjs"
version = "0.10.1" version = "0.10.0"
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.150" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1170,9 +1170,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.12.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1527984ca054dfca79333baec451042863f485fbee01b7bf6d911de915cac865" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
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.72" version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "wasm-bindgen-test" name = "wasm-bindgen-test"
version = "0.3.72" version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fde991ccdc895cb7fbaa14b137d62af74d9011be67b71c694bfc40edd3119c" checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0"
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.72" version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e925354648d2a4d1bf205412e36d520a800280622eef4719678d268e5d40e978" checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb"
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.122" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "684365b586a9a6256c1cc3544eee8680de48d6041142f581776ec7b139622ae9" checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527"
[[package]] [[package]]
name = "wasm-encoder" name = "wasm-encoder"
@@ -1672,18 +1672,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.49" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1692,9 +1692,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -2,7 +2,7 @@
[package] [package]
name = "regorusjs" name = "regorusjs"
version = "0.10.1" version = "0.10.0"
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.150" serde_json = "1.0.140"
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.72" wasm-bindgen-test = "0.3.71"
[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)'] }

View File

@@ -21,3 +21,9 @@ Run `cargo xtask build-wasm` to invoke wasm-pack with sensible defaults, or `car
## Usage ## Usage
See [test.js](https://github.com/microsoft/regorus/blob/main/bindings/wasm/test.js) for example usage. See [test.js](https://github.com/microsoft/regorus/blob/main/bindings/wasm/test.js) for example usage.
For best performance with large policies, call `engine.prepare()` after loading
policy/data, then use `engine.clone()` to create per-request engines. If
`prepare()` is skipped, the first `eval*` call performs the same one-time
setup. Adding/changing policy or data after `prepare()` invalidates the
prepared state.

View File

@@ -138,6 +138,17 @@ impl Engine {
self.engine.set_rego_v0(enable) self.engine.set_rego_v0(enable)
} }
/// Clone this engine.
///
/// Useful for creating per-request engines after loading policy/data once.
///
/// Clone is designed to avoid reparsing policy text and reloading immutable
/// policy structures. Mutable evaluation state is copied for isolation.
#[wasm_bindgen(js_name = "clone")]
pub fn cloneEngine(&self) -> Engine {
Clone::clone(self)
}
/// Add a policy /// Add a policy
/// ///
/// The policy is parsed into AST. /// The policy is parsed into AST.
@@ -158,6 +169,20 @@ impl Engine {
self.engine.add_data(data).map_err(error_to_jsvalue) self.engine.add_data(data).map_err(error_to_jsvalue)
} }
/// Prepare the engine for evaluation.
///
/// The first evaluation on an unprepared engine performs one-time setup.
/// Calling `prepare()` performs that setup eagerly.
///
/// This is optional for correctness. If omitted, the first `eval*` call
/// implicitly performs preparation.
///
/// If policies/data are modified after `prepare()`, preparation is
/// invalidated and must be performed again (explicitly or via first eval).
pub fn prepare(&mut self) -> Result<(), JsValue> {
self.engine.prepare().map_err(error_to_jsvalue)
}
/// Get the list of packages defined by loaded policies. /// Get the list of packages defined by loaded policies.
/// ///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages
@@ -487,6 +512,9 @@ mod tests {
)?; )?;
assert_eq!(pkg, "data.test"); assert_eq!(pkg, "data.test");
// Prepare before first evaluation.
engine.prepare()?;
let results = engine.evalQuery("data".to_string())?; let results = engine.evalQuery("data".to_string())?;
let r = regorus::Value::from_json_str(&results).map_err(error_to_jsvalue)?; let r = regorus::Value::from_json_str(&results).map_err(error_to_jsvalue)?;

View File

@@ -40,6 +40,13 @@ engine.addDataJson(`
} }
`); `);
// Prepare internal evaluation structures once.
engine.prepare();
// Clone a prepared template engine for reuse.
var template = engine.clone();
engine = template.clone();
// Set policy input // Set policy input
engine.setInputJson(` engine.setInputJson(`
{ {

View File

@@ -1,84 +0,0 @@
# 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.

View File

@@ -23,7 +23,8 @@ use regorus::languages::azure_policy::aliases::AliasRegistry;
use regorus::languages::azure_policy::compiler; use regorus::languages::azure_policy::compiler;
use regorus::languages::azure_policy::parser; use regorus::languages::azure_policy::parser;
use regorus::rvm::RegoVM; use regorus::rvm::RegoVM;
use regorus::{Rc, Source, Value}; use regorus::Source;
use regorus::Value;
/// Evaluate an Azure Policy definition against a resource. /// Evaluate an Azure Policy definition against a resource.
/// ///
@@ -59,8 +60,11 @@ pub fn azure_policy_eval(
println!("Parsed policy definition from {policy_definition}"); println!("Parsed policy definition from {policy_definition}");
// 3. Compile to RVM bytecode. // 3. Compile to RVM bytecode.
let registry = Rc::new(registry); let program = compiler::compile_policy_definition_with_aliases(
let program = compiler::compile_policy_definition_with_aliases(&defn, Rc::clone(&registry))?; &defn,
registry.alias_map(),
registry.alias_modifiable_map(),
)?;
println!("Compiled policy to RVM bytecode"); println!("Compiled policy to RVM bytecode");
// 4. Build normalized input. // 4. Build normalized input.
@@ -134,7 +138,7 @@ pub fn azure_policy_aliases(aliases: String, resource_type: Option<String>) -> R
if let Some(ref rt) = resource_type { if let Some(ref rt) = resource_type {
let rt_lower = rt.to_lowercase(); let rt_lower = rt.to_lowercase();
let mut found = false; let mut found = false;
for alias_name in registry.alias_map().keys() { for (alias_name, _) in registry.alias_map() {
if alias_name.to_lowercase().starts_with(&rt_lower) { if alias_name.to_lowercase().starts_with(&rt_lower) {
println!(" {alias_name}"); println!(" {alias_name}");
found = true; found = true;
@@ -144,7 +148,7 @@ pub fn azure_policy_aliases(aliases: String, resource_type: Option<String>) -> R
bail!("no aliases found for resource type '{rt}'"); bail!("no aliases found for resource type '{rt}'");
} }
} else { } else {
for alias_name in registry.alias_map().keys() { for (alias_name, _) in registry.alias_map() {
println!(" {alias_name}"); println!(" {alias_name}");
} }
} }

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.7" version = "2.2.6"
license = "MIT" license = "MIT"
repository = "https://github.com/microsoft/regorus" repository = "https://github.com/microsoft/regorus"

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_sorted() { for (key, value) in map.iter() {
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());

View File

@@ -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: Object = first.as_ref().clone(); let mut result: BTreeMap<Value, Value> = 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 = Object::new(); let mut result = BTreeMap::<Value, Value>::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 = Object::new(); let mut map = BTreeMap::<Value, Value>::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: &Object, overlay: &Object) -> Value { fn merge_objects(base: &BTreeMap<Value, Value>, overlay: &BTreeMap<Value, Value>) -> Value {
let mut result = base.clone(); let mut result = base.clone();
for (k, v) in overlay.iter() { for (k, v) in overlay {
#[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),

View File

@@ -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.iter_sorted() { for (k, v) in obj.as_ref() {
let mut entry = Object::new(); let mut entry = BTreeMap::<Value, Value>::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)));

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_sorted() { for (key, value) in obj.iter() {
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) => {

View File

@@ -7,11 +7,10 @@ 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::BTreeSet; use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -81,7 +80,7 @@ fn reachable(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) ->
} }
fn visit( fn visit(
graph: &Object, graph: &BTreeMap<Value, Value>,
visited: &mut BTreeSet<Value>, visited: &mut BTreeSet<Value>,
node: &Value, node: &Value,
path: &mut Vec<Value>, path: &mut Vec<Value>,
@@ -212,7 +211,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_sorted() { for (key, value) in obj.iter() {
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()?;

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.get_or_insert_with(p.clone(), Value::new_object); let entry = obj.entry(p.clone()).or_insert_with(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

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_sorted() + &o.iter()
.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_sorted() { for item in obj.as_ref().iter() {
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();

View File

@@ -5,12 +5,11 @@
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::BTreeSet; use alloc::collections::{BTreeMap, BTreeSet};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -169,7 +168,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<Object>> { pub fn ensure_object(fcn: &str, arg: &Expr, v: Value) -> Result<Rc<BTreeMap<Value, Value>>> {
Ok(match v { Ok(match v {
Value::Object(o) => o, Value::Object(o) => o,
_ => { _ => {

View File

@@ -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); scheduled.extend(binds.into_iter());
ordered.push((value_expr, plan)); ordered.push((value_expr, plan));
progress = true; progress = true;
break; break;

View File

@@ -505,6 +505,47 @@ impl Engine {
self.add_data(Value::from_json_str(data_json)?) self.add_data(Value::from_json_str(data_json)?)
} }
/// Prepare the engine for evaluation without executing a query or rule.
///
/// The first evaluation on an unprepared engine performs one-time setup
/// (analysis, scheduling, imports/rules processing, and initialization of
/// internal evaluation structures). Calling this method performs that work
/// eagerly so a later call to [`Engine::eval_rule`] / [`Engine::eval_query`]
/// does not pay that startup cost.
///
/// This method is optional for correctness. If omitted, the first
/// evaluation will implicitly prepare the engine.
///
/// Preparation is invalidated when policy/data that affects evaluation is
/// changed (for example: [`Engine::add_policy`], [`Engine::add_policy_from_file`],
/// [`Engine::add_data`], [`Engine::clear_data`]). In those cases, the next
/// evaluation (or another explicit call to `prepare`) performs setup again.
///
/// This is especially useful before cloning template engines used for
/// repeated evaluations.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
/// engine.add_policy("test.rego".to_string(), r#"
/// package test
/// import rego.v1
/// allow if input.user == "alice"
/// "#.to_string())?;
///
/// engine.prepare()?;
/// let mut cloned = engine.clone();
///
/// cloned.set_input_json(r#"{"user":"alice"}"#)?;
/// assert_eq!(cloned.eval_rule("data.test.allow".to_string())?, Value::from(true));
/// # Ok(())
/// # }
/// ```
pub fn prepare(&mut self) -> Result<()> {
self.prepare_for_eval(false, false)
}
/// Set whether builtins should raise errors strictly or not. /// Set whether builtins should raise errors strictly or not.
/// ///
/// Regorus differs from OPA in that by default builtins will /// Regorus differs from OPA in that by default builtins will
@@ -1084,9 +1125,10 @@ impl Engine {
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?; limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
self.interpreter.set_traces(enable_tracing); self.interpreter.set_traces(enable_tracing);
let newly_prepared = !self.prepared;
// if the data/policies have changed or the interpreter has never been prepared // if the data/policies have changed or the interpreter has never been prepared
if !self.prepared { if newly_prepared {
// Analyze the modules and determine how statements must be scheduled. // Analyze the modules and determine how statements must be scheduled.
let analyzer = Analyzer::new(); let analyzer = Analyzer::new();
let schedule = Rc::new(analyzer.analyze(&self.modules)?); let schedule = Rc::new(analyzer.analyze(&self.modules)?);
@@ -1116,23 +1158,28 @@ impl Engine {
// Set schedule after hoisting completes // Set schedule after hoisting completes
self.interpreter.set_schedule(Some(schedule)); self.interpreter.set_schedule(Some(schedule));
}
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
{
if for_target { if for_target {
// Resolve and validate target specifications across all modules // Resolve and validate target specifications across all modules.
// This must run for target-aware compilation even if generic prepare()
// was already called.
crate::interpreter::target::resolve::resolve_and_apply_target( crate::interpreter::target::resolve::resolve_and_apply_target(
&mut self.interpreter, &mut self.interpreter,
)?; )?;
// Infer resource types // Infer resource types
crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?; crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?;
} } else if newly_prepared {
// Check if any module specifies a target and warn if so.
if !for_target {
// Check if any module specifies a target and warn if so
#[cfg(feature = "azure_policy")]
self.warn_if_targets_present(); self.warn_if_targets_present();
} }
}
#[cfg(not(feature = "azure_policy"))]
let _ = for_target;
if newly_prepared {
self.prepared = true; self.prepared = true;
} }

View File

@@ -28,6 +28,7 @@ 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::*;
@@ -1311,10 +1312,10 @@ impl Interpreter {
*obj = Value::new_object(); *obj = Value::new_object();
} }
obj = obj.as_object_mut()?.get_or_insert_with( obj = obj
Value::String(p.to_string().into()), .as_object_mut()?
Value::new_object, .entry(Value::String(p.to_string().into()))
); .or_insert(Value::new_object());
} }
*obj = value; *obj = value;
// Mark modified rules as processed. // Mark modified rules as processed.
@@ -1681,7 +1682,8 @@ 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")))?
.get_or_insert_with(p, Value::new_set) .entry(p)
.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()?);
@@ -1689,13 +1691,20 @@ 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")))?;
if value == Value::Undefined { match obj.entry(p) {
// TODO: clean this assumption between Undefined vs Object. BTreeMapEntry::Vacant(v) => {
obj.get_or_insert_with(p, Value::new_object); if value != Value::Undefined {
} else { v.insert(value);
let existing = obj.get_or_insert_with(p, || value.clone()); } else {
if *existing != value { // TODO: clean this assumption between Undefined vs Object.
bail!(span.error("complete rules should not produce multiple outputs")) v.insert(Value::new_object());
}
}
BTreeMapEntry::Occupied(o) => {
if o.get() != &value && value != Value::Undefined {
bail!(span
.error("complete rules should not produce multiple outputs"))
}
} }
} }
} }
@@ -1704,7 +1713,8 @@ 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")))?
.get_or_insert_with(p, Value::new_object); .entry(p)
.or_insert(Value::new_object());
} }
} }
Ok(()) Ok(())
@@ -1772,7 +1782,6 @@ impl Interpreter {
let mut comps = self.eval_rule_ref(&rule_ref)?; let mut comps = self.eval_rule_ref(&rule_ref)?;
if let Some(ke) = &key_expr { if let Some(ke) = &key_expr {
is_const_rule = is_const_rule && Self::is_simple_literal(ke)?;
comps.push(self.eval_expr(ke)?); comps.push(self.eval_expr(ke)?);
} }
let output = if let Some(oe) = &output_expr { let output = if let Some(oe) = &output_expr {
@@ -1812,7 +1821,8 @@ impl Interpreter {
let set = ctx_mut let set = ctx_mut
.rule_value .rule_value
.as_object_mut()? .as_object_mut()?
.get_or_insert_with(Value::from_array(comps), Value::new_set); .entry(Value::from_array(comps))
.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);
@@ -1821,13 +1831,20 @@ impl Interpreter {
} }
// Non-set rule. // Non-set rule.
let key = Value::from_array(comps); match ctx_mut
let obj_mut = ctx_mut.rule_value.as_object_mut()?; .rule_value
let existing = obj_mut.get_or_insert_with(key, || output.clone()); .as_object_mut()?
if *existing != output { .entry(Value::from_array(comps))
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);
@@ -2453,7 +2470,7 @@ impl Interpreter {
} }
Value::Object(map) => { Value::Object(map) => {
s.push('{'); s.push('{');
for (idx, (k, entry_value)) in map.iter_sorted().enumerate() { for (idx, (k, entry_value)) in map.iter().enumerate() {
if idx > 0 { if idx > 0 {
s.push_str(", "); s.push_str(", ");
} }

View File

@@ -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 Object, avoiding full ObjMap round-trip. // Merge directly into the BTreeMap, 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.get_or_insert_with(Value::String(k), || v); existing.entry(Value::String(k)).or_insert(v);
} }
} else { } else {
obj_insert(&mut result, "properties", make_value(properties)); obj_insert(&mut result, "properties", make_value(properties));

View File

@@ -7,7 +7,6 @@ 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};
@@ -142,7 +141,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 Object, btree: &mut alloc::collections::BTreeMap<Value, Value>,
parent_parts: &[&str], parent_parts: &[&str],
array_name: &str, array_name: &str,
envelope_fields: &BTreeSet<String>, envelope_fields: &BTreeSet<String>,
@@ -188,7 +187,10 @@ 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(btree: &Object, key: &str) -> Option<Value> { fn find_key_ci_btree(
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)))

View File

@@ -172,31 +172,11 @@ impl AliasRegistry {
let prefix = alloc::format!("{}/", fq_type); let prefix = alloc::format!("{}/", fq_type);
for alias in aliases { for alias in aliases {
// Skip aliases without a default_path — the normalizer's
// resolve_resource_type also skips these, so inserting them into
// compiler maps would cause a divergence where the compiler
// resolves the alias but normalized input never contains the field.
if alias.default_path.is_none() {
continue;
}
// Derive the short name by stripping the resource type prefix. // Derive the short name by stripping the resource type prefix.
let raw_short = if alias.name.len() > prefix.len() let raw_short = if alias.name.len() > prefix.len()
&& alias && alias.name[..prefix.len()].eq_ignore_ascii_case(&prefix)
.name
.get(..prefix.len())
.is_some_and(|s| s.eq_ignore_ascii_case(&prefix))
{ {
// Both slice boundaries are valid: prefix is ASCII alias.name[prefix.len()..].to_string()
// (resource type + '/'), so if `..prefix.len()` succeeded
// above, `prefix.len()..` is guaranteed to be on a char
// boundary too. The `unwrap_or` is a defensive fallback
// that can never trigger for well-formed Azure alias names.
alias
.name
.get(prefix.len()..)
.unwrap_or(&alias.name)
.to_string()
} else if let Some(rest) = alias } else if let Some(rest) = alias
.name .name
.rfind('/') .rfind('/')
@@ -280,19 +260,20 @@ impl AliasRegistry {
.map(String::as_str) .map(String::as_str)
} }
/// Return a reference to the alias-to-short-name map. /// Return a clone of the alias-to-short-name map for use by the compiler.
/// ///
/// Keys are lowercase fully-qualified alias names; values are short names. /// The compiler stores this map internally so it can resolve fully-qualified
pub const fn alias_map(&self) -> &BTreeMap<String, String> { /// alias names without holding a reference to the registry.
&self.alias_to_short pub fn alias_map(&self) -> BTreeMap<String, String> {
self.alias_to_short.clone()
} }
/// Return a reference to the alias-to-modifiable map. /// Return a clone of the alias-to-modifiable map for use by the compiler.
/// ///
/// Keys are lowercase fully-qualified alias names; values are `true` when /// Maps lowercase fully-qualified alias names to `true` when the alias
/// the alias has `defaultMetadata.attributes = "Modifiable"`. /// has `defaultMetadata.attributes = "Modifiable"`.
pub const fn alias_modifiable_map(&self) -> &BTreeMap<String, bool> { pub fn alias_modifiable_map(&self) -> BTreeMap<String, bool> {
&self.alias_modifiable self.alias_modifiable.clone()
} }
/// Normalize a raw ARM resource and wrap it in the input envelope. /// Normalize a raw ARM resource and wrap it in the input envelope.

View File

@@ -4,13 +4,14 @@
//! Per-alias path resolution: reads values from versioned ARM paths and places //! Per-alias path resolution: reads values from versioned ARM paths and places
//! them at alias short name paths in the normalized output. //! them at alias short name paths in the normalized output.
use crate::Rc; use alloc::string::String;
use crate::Value; use crate::Value;
use super::super::obj_map::remove_element_field; use super::super::obj_map::remove_element_field;
use super::super::obj_map::{ use super::super::obj_map::{
collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_insert_rc, collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_remove,
obj_remove, set_nested_lowercased, ObjMap, set_nested_lowercased, ObjMap,
}; };
use super::super::types::ResolvedAliases; use super::super::types::ResolvedAliases;
use super::element_remap::apply_element_remap_precomputed; use super::element_remap::apply_element_remap_precomputed;
@@ -47,14 +48,12 @@ pub fn apply_alias_entries(
if let Some(value) = value { if let Some(value) = value {
let value = normalize_value(&value, &entry.short_name, None); let value = normalize_value(&value, &entry.short_name, None);
if is_root_field_collision(&entry.short_name, &entry.default_path) { let target = if is_root_field_collision(&entry.short_name, &entry.default_path) {
let target = collision_safe_key(&entry.short_name); collision_safe_key(&entry.short_name)
set_nested_lowercased(result, &target, value);
} else if entry.short_name.contains('.') {
set_nested_lowercased(result, &entry.short_name, value);
} else { } else {
obj_insert_rc(result, Rc::clone(&entry.short_name_lc), value); entry.short_name.clone()
} };
set_nested_lowercased(result, &target, value);
} }
} }
@@ -85,13 +84,13 @@ pub fn apply_alias_entries(
} }
/// Navigate an ARM path using precomputed segments (avoids per-call split). /// Navigate an ARM path using precomputed segments (avoids per-call split).
fn navigate_arm_path_segments(value: &Value, segments: &[Rc<str>]) -> Option<Value> { fn navigate_arm_path_segments(value: &Value, segments: &[String]) -> Option<Value> {
let mut current = value; let mut current = value;
for segment in segments { for segment in segments {
current = current current = current
.as_object() .as_object()
.ok()? .ok()?
.get(&Value::String(Rc::clone(segment)))?; .get(&Value::from(segment.as_str()))?;
} }
Some(current.clone()) Some(current.clone())
} }

View File

@@ -6,12 +6,11 @@
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, set_nested_lowercased, set_nested_verbatim, obj_get, obj_get_mut, obj_insert, set_nested_in_btree, set_nested_lowercased,
ObjMap, set_nested_verbatim, ObjMap,
}; };
use super::super::types::PrecomputedRemap; use super::super::types::PrecomputedRemap;
@@ -119,7 +118,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 Object, btree: &mut alloc::collections::BTreeMap<Value, Value>,
array_chain: &[Vec<String>], array_chain: &[Vec<String>],
depth: usize, depth: usize,
source_field: &str, source_field: &str,
@@ -178,7 +177,12 @@ 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(btree: &mut Object, source: &str, target: &str, lowercase: bool) { fn remap_deep_field_in_btree(
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,
@@ -194,11 +198,14 @@ fn remap_deep_field_in_btree(btree: &mut Object, source: &str, target: &str, low
} }
return; return;
} }
set_nested(btree, &segments, val, lowercase); set_nested_in_btree(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(btree: &Object, path: &str) -> Option<Value> { fn read_dotted_path_btree(
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))?;

View File

@@ -13,7 +13,6 @@ 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::{
@@ -110,7 +109,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: &Object, obj: &alloc::collections::BTreeMap<Value, Value>,
result: &mut ObjMap, result: &mut ObjMap,
sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>, sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>,
) { ) {

View File

@@ -4,15 +4,14 @@
//! 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` (an `Object`) only at //! then converts to `Value::Object` (a `BTreeMap<Value, Value>`) only at
//! the output boundary via [`make_value`]. //! the output boundary via [`make_value`].
use alloc::string::String; use alloc::string::{String, ToString as _};
use alloc::vec::Vec; 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;
@@ -42,33 +41,6 @@ pub fn obj_insert(map: &mut ObjMap, key: &str, val: Value) {
map.insert(Rc::from(key), val); map.insert(Rc::from(key), val);
} }
/// Insert a key-value pair using a pre-allocated `Rc<str>` key.
///
/// Avoids the `Rc::from(key)` heap allocation that [`obj_insert`] performs.
pub fn obj_insert_rc(map: &mut ObjMap, key: Rc<str>, val: Value) {
map.insert(key, val);
}
/// Lowercase a string, returning an `Rc<str>`.
///
/// Both paths allocate an `Rc<str>` (header + string bytes). The fast-path
/// avoids creating an intermediate lowercased `String` when the input is
/// already all-lowercase ASCII.
pub fn rc_lowercase(s: &str) -> Rc<str> {
if s.bytes().all(|b| !b.is_ascii_uppercase()) {
Rc::from(s)
} else {
Rc::from(s.to_ascii_lowercase())
}
}
/// Insert a key-value pair with the key lowercased, using [`rc_lowercase`]
/// for the allocation fast-path.
pub fn obj_insert_lc(map: &mut ObjMap, key: &str, val: Value) {
let lc = rc_lowercase(key);
map.insert(lc, val);
}
/// Check whether a key exists. /// Check whether a key exists.
pub fn obj_contains(map: &ObjMap, key: &str) -> bool { pub fn obj_contains(map: &ObjMap, key: &str) -> bool {
map.contains_key(key) map.contains_key(key)
@@ -82,13 +54,14 @@ 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
/// an `Object` to match the `Value::Object` representation. /// a `BTreeMap` to match the `Value::Object` representation.
pub fn make_value(map: ObjMap) -> Value { pub fn make_value(map: ObjMap) -> Value {
let obj: Object = map use alloc::collections::BTreeMap;
.into_iter() let mut btree = BTreeMap::new();
.map(|(k, v)| (Value::String(k), v)) for (k, v) in map {
.collect(); btree.insert(Value::String(k), v);
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 +88,14 @@ pub fn extract_type_field(resource: &Value) -> Option<&str> {
}) })
} }
/// Convert a `Value::Object` (Object) into an [`ObjMap`]. /// Convert a `Value::Object` (BTreeMap<Value, Value>) 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 obj = value.as_object().ok()?; let btree = value.as_object().ok()?;
let mut map = ObjMap::with_capacity(obj.len()); let mut map = ObjMap::with_capacity(btree.len());
for (k, v) in obj.iter() { for (k, v) in btree.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());
} }
@@ -139,7 +112,7 @@ pub fn set_nested_lowercased(result: &mut ObjMap, path: &str, value: Value) {
} }
if segments.len() == 1 { if segments.len() == 1 {
if let Some(&seg) = segments.first() { if let Some(&seg) = segments.first() {
obj_insert_lc(result, seg, value); obj_insert(result, &seg.to_ascii_lowercase(), value);
} }
return; return;
} }
@@ -171,30 +144,30 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
}; };
if segments.len() == 1 { if segments.len() == 1 {
let key: Rc<str> = if lowercase { let key = if lowercase {
rc_lowercase(first) first.to_ascii_lowercase()
} else { } else {
Rc::from(first) first.to_string()
}; };
obj_insert_rc(obj, key, value); obj_insert(obj, &key, value);
return; return;
} }
let seg: Rc<str> = if lowercase { let seg = if lowercase {
rc_lowercase(first) first.to_ascii_lowercase()
} else { } else {
Rc::from(first) first.to_string()
}; };
// Ensure an intermediate object exists at `seg`. // Ensure an intermediate object exists at `seg`.
if !obj.contains_key(&*seg) { if !obj_contains(obj, &seg) {
obj_insert_rc(obj, Rc::clone(&seg), make_value(new_map())); obj_insert(obj, &seg, make_value(new_map()));
} }
// 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(obj, &seg) {
let inner_btree = Rc::make_mut(inner_rc); let inner_btree = Rc::make_mut(inner_rc);
set_nested( set_nested_in_btree(
inner_btree, inner_btree,
segments.get(1..).unwrap_or_default(), segments.get(1..).unwrap_or_default(),
value, value,
@@ -203,36 +176,41 @@ fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase
} }
} }
/// Set a value at a path directly in an `Object`, creating /// Set a value at a path directly in a `BTreeMap<Value, Value>`, 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(obj: &mut Object, segments: &[&str], value: Value, lowercase: bool) { pub fn set_nested_in_btree(
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;
}; };
let key_rc: Rc<str> = if lowercase { let key_str: String = if lowercase {
rc_lowercase(first) first.to_ascii_lowercase()
} else { } else {
Rc::from(first) first.to_string()
}; };
let key_val = Value::String(Rc::clone(&key_rc)); let key_val = Value::String(Rc::from(key_str.as_str()));
if segments.len() == 1 { if segments.len() == 1 {
obj.insert(key_val, value); btree.insert(key_val, value);
return; return;
} }
// Ensure an intermediate object exists. // Ensure an intermediate object exists.
if !obj.contains_key(&key_val) { if !btree.contains_key(&key_val) {
obj.insert(key_val.clone(), make_value(new_map())); btree.insert(key_val.clone(), make_value(new_map()));
} }
if let Some(Value::Object(inner_rc)) = obj.get_mut(&key_val) { if let Some(Value::Object(inner_rc)) = btree.get_mut(&key_val) {
let inner = Rc::make_mut(inner_rc); let inner = Rc::make_mut(inner_rc);
set_nested( set_nested_in_btree(
inner, inner,
segments.get(1..).unwrap_or_default(), segments.get(1..).unwrap_or_default(),
value, value,
@@ -265,24 +243,13 @@ pub const ROOT_FIELDS: &[&str] = &[
"extendedLocation", "extendedLocation",
]; ];
const PROPERTIES_DOT: &[u8] = b"properties.";
/// Check whether an alias short name collides with a reserved ARM root field /// Check whether an alias short name collides with a reserved ARM root field
/// and needs a collision-safe key. /// and needs a collision-safe key.
pub fn is_root_field_collision(short_name: &str, default_path: &str) -> bool { pub fn is_root_field_collision(short_name: &str, default_path: &str) -> bool {
ROOT_FIELDS ROOT_FIELDS
.iter() .iter()
.any(|f| f.eq_ignore_ascii_case(short_name)) .any(|f| f.eq_ignore_ascii_case(short_name))
&& default_path.len() > PROPERTIES_DOT.len() && default_path.to_ascii_lowercase().starts_with("properties.")
&& default_path
.as_bytes()
.get(..PROPERTIES_DOT.len())
.is_some_and(|prefix| {
prefix
.iter()
.zip(PROPERTIES_DOT)
.all(|(a, b)| a.to_ascii_lowercase() == *b)
})
} }
/// Return a collision-safe key for an alias whose short name collides with a /// Return a collision-safe key for an alias whose short name collides with a
@@ -347,15 +314,20 @@ 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_obj(inner_btree, array_chain, depth.saturating_add(1), field); remove_field_at_depth_in_btree(
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
} }
} }
} }
} }
/// Object-native recursion for element-level field removal. /// BTreeMap-native recursion for element-level field removal.
fn remove_field_at_depth_obj( fn remove_field_at_depth_in_btree(
obj: &mut Object, btree: &mut alloc::collections::BTreeMap<Value, Value>,
array_chain: &[Vec<String>], array_chain: &[Vec<String>],
depth: usize, depth: usize,
field: &str, field: &str,
@@ -364,10 +336,10 @@ fn remove_field_at_depth_obj(
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() {
obj.remove(&Value::from(seg)); btree.remove(&Value::from(seg));
} }
} else if segments.len() > 1 { } else if segments.len() > 1 {
remove_at_dotted_path_obj(obj, &segments); remove_at_dotted_path_in_btree(btree, &segments);
} }
return; return;
}; };
@@ -379,12 +351,12 @@ fn remove_field_at_depth_obj(
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 obj.get_mut(&key_val) { match btree.get_mut(&key_val) {
Some(v) => v, Some(v) => v,
None => return, None => return,
} }
} else { } else {
let mut cur: &mut Value = match obj.get_mut(&key_val) { let mut cur: &mut Value = match btree.get_mut(&key_val) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };
@@ -405,19 +377,27 @@ fn remove_field_at_depth_obj(
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_obj(inner_btree, array_chain, depth.saturating_add(1), field); remove_field_at_depth_in_btree(
inner_btree,
array_chain,
depth.saturating_add(1),
field,
);
} }
} }
} }
} }
/// Remove the leaf segment at a dotted path directly in an Object. /// Remove the leaf segment at a dotted path directly in a BTreeMap.
fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) { fn remove_at_dotted_path_in_btree(
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() {
obj.remove(&Value::from(leaf)); btree.remove(&Value::from(leaf));
return; return;
} }
@@ -425,7 +405,7 @@ fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) {
return; return;
}; };
let first_key = Value::from(first); let first_key = Value::from(first);
let parent_val = match obj.get_mut(&first_key) { let parent_val = match btree.get_mut(&first_key) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };

View File

@@ -18,22 +18,6 @@ use alloc::vec::Vec;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use crate::Rc;
// ---------------------------------------------------------------------------
// Deserialization helpers
// ---------------------------------------------------------------------------
/// Deserialize a `Vec<T>` that tolerates JSON `null` by mapping it to an
/// empty vector.
fn deserialize_null_as_empty_vec<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
}
// ─── Top-level response wrappers ──────────────────────────────────────────── // ─── Top-level response wrappers ────────────────────────────────────────────
/// ARM API response envelope: `{ "value": [...] }` /// ARM API response envelope: `{ "value": [...] }`
@@ -114,10 +98,7 @@ pub struct AliasEntry {
/// Versioned path entries. Empty for the vast majority of aliases that /// Versioned path entries. Empty for the vast majority of aliases that
/// have only a `defaultPath`. /// have only a `defaultPath`.
/// #[serde(default)]
/// In real Azure catalog data (~97% of aliases), `az provider list` emits
/// `"paths": null` rather than an empty array.
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
pub paths: Vec<AliasPath>, pub paths: Vec<AliasPath>,
} }
@@ -423,13 +404,11 @@ pub struct ResolvedEntry {
// ── Precomputed fields (derived at registry-load time) ────────────── // ── Precomputed fields (derived at registry-load time) ──────────────
/// Whether `short_name` contains `[*]` (i.e., this is a wildcard/array alias). /// Whether `short_name` contains `[*]` (i.e., this is a wildcard/array alias).
pub is_wildcard: bool, pub is_wildcard: bool,
/// Pre-lowercased short name as `Rc<str>` for allocation-free common-case inserts.
pub(crate) short_name_lc: Rc<str>,
/// Precomputed `default_path.split('.').collect()` for fast ARM path navigation. /// Precomputed `default_path.split('.').collect()` for fast ARM path navigation.
pub(crate) default_path_segments: Vec<Rc<str>>, pub default_path_segments: Vec<String>,
/// Precomputed path segments for each versioned path, in the same order /// Precomputed path segments for each versioned path, in the same order
/// as `versioned_paths`. /// as `versioned_paths`.
pub(crate) versioned_path_segments: Vec<Vec<Rc<str>>>, pub versioned_path_segments: Vec<Vec<String>>,
} }
impl ResolvedEntry { impl ResolvedEntry {
@@ -441,15 +420,10 @@ impl ResolvedEntry {
metadata: Option<AliasPathMetadata>, metadata: Option<AliasPathMetadata>,
) -> Self { ) -> Self {
let is_wildcard = short_name.contains("[*]"); let is_wildcard = short_name.contains("[*]");
let short_name_lc = if short_name.bytes().all(|b| !b.is_ascii_uppercase()) { let default_path_segments = default_path.split('.').map(String::from).collect();
Rc::from(short_name.as_str())
} else {
Rc::from(short_name.to_ascii_lowercase())
};
let default_path_segments = default_path.split('.').map(Rc::from).collect();
let versioned_path_segments = versioned_paths let versioned_path_segments = versioned_paths
.iter() .iter()
.map(|(_, p)| p.split('.').map(Rc::from).collect()) .map(|(_, p)| p.split('.').map(String::from).collect())
.collect(); .collect();
Self { Self {
short_name, short_name,
@@ -457,7 +431,6 @@ impl ResolvedEntry {
versioned_paths, versioned_paths,
metadata, metadata,
is_wildcard, is_wildcard,
short_name_lc,
default_path_segments, default_path_segments,
versioned_path_segments, versioned_path_segments,
} }
@@ -483,7 +456,7 @@ impl ResolvedEntry {
/// Returns the versioned segments if `api_version` matches, otherwise /// Returns the versioned segments if `api_version` matches, otherwise
/// the default segments. This avoids per-call `split('.')` for both /// the default segments. This avoids per-call `split('.')` for both
/// default and versioned scalar alias navigation. /// default and versioned scalar alias navigation.
pub(crate) fn select_path_segments(&self, api_version: Option<&str>) -> &[Rc<str>] { pub fn select_path_segments(&self, api_version: Option<&str>) -> &[String] {
if let Some(ver) = api_version { if let Some(ver) = api_version {
for (i, (v, _)) in self.versioned_paths.iter().enumerate() { for (i, (v, _)) in self.versioned_paths.iter().enumerate() {
if v.eq_ignore_ascii_case(ver) { if v.eq_ignore_ascii_case(ver) {

View File

@@ -18,7 +18,6 @@ use crate::rvm::program::{Program, SpanInfo};
use crate::rvm::Instruction; use crate::rvm::Instruction;
use crate::{Rc, Value}; use crate::{Rc, Value};
use crate::languages::azure_policy::aliases::AliasRegistry;
use crate::languages::azure_policy::ast::PolicyRule; use crate::languages::azure_policy::ast::PolicyRule;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -45,9 +44,10 @@ pub(super) struct Compiler {
pub(super) cached_input_reg: Option<u8>, pub(super) cached_input_reg: Option<u8>,
/// Cached register for `LoadContext` — allocated once on first use. /// Cached register for `LoadContext` — allocated once on first use.
pub(super) cached_context_reg: Option<u8>, pub(super) cached_context_reg: Option<u8>,
/// Alias registry for resolving fully-qualified alias names. /// Map from lowercase fully-qualified alias name → short name.
/// Shared via `Rc` to avoid cloning the 73K-entry alias maps. pub(super) alias_map: BTreeMap<String, String>,
pub(super) alias_registry: Option<Rc<AliasRegistry>>, /// Map from lowercase fully-qualified alias name → modifiable flag.
pub(super) alias_modifiable: BTreeMap<String, bool>,
/// Default values for policy parameters. /// Default values for policy parameters.
pub(super) parameter_defaults: Option<Value>, pub(super) parameter_defaults: Option<Value>,
/// Cached literal-table index for `parameter_defaults` (or an empty object /// Cached literal-table index for `parameter_defaults` (or an empty object
@@ -338,13 +338,8 @@ impl Compiler {
path: &str, path: &str,
span: &crate::lexer::Span, span: &crate::lexer::Span,
) -> Result<String> { ) -> Result<String> {
let alias_map = match &self.alias_registry {
Some(reg) => reg.alias_map(),
None => return Ok(path.to_string()),
};
let lc = path.to_ascii_lowercase(); let lc = path.to_ascii_lowercase();
if let Some(short) = alias_map.get(&lc) { if let Some(short) = self.alias_map.get(&lc) {
let resolved = short.clone(); let resolved = short.clone();
let result = Self::strip_fq_prefix(&resolved).to_ascii_lowercase(); let result = Self::strip_fq_prefix(&resolved).to_ascii_lowercase();
return Ok(result); return Ok(result);
@@ -353,7 +348,7 @@ impl Compiler {
// Fallback: derive array path from a corresponding `[*]` alias. // Fallback: derive array path from a corresponding `[*]` alias.
if !lc.contains("[*]") { if !lc.contains("[*]") {
let wildcard_key = alloc::format!("{}[*]", lc); let wildcard_key = alloc::format!("{}[*]", lc);
if let Some(short) = alias_map.get(&wildcard_key) { if let Some(short) = self.alias_map.get(&wildcard_key) {
let resolved = Self::strip_fq_prefix(short).to_ascii_lowercase(); let resolved = Self::strip_fq_prefix(short).to_ascii_lowercase();
if let Some(base) = resolved.strip_suffix("[*]") { if let Some(base) = resolved.strip_suffix("[*]") {
return Ok(base.to_string()); return Ok(base.to_string());
@@ -361,14 +356,14 @@ impl Compiler {
} }
} }
if !alias_map.is_empty() && !self.alias_fallback_to_raw { if !self.alias_map.is_empty() && !self.alias_fallback_to_raw {
bail!(span.error(&alloc::format!( bail!(span.error(&alloc::format!(
"unknown alias '{}': field references must use fully-qualified alias names when an alias catalog is loaded", "unknown alias '{}': field references must use fully-qualified alias names when an alias catalog is loaded",
path path
))); )));
} }
if alias_map.is_empty() { if self.alias_map.is_empty() {
Ok(path.to_string()) Ok(path.to_string())
} else { } else {
let result = Self::strip_fq_prefix(path).to_ascii_lowercase(); let result = Self::strip_fq_prefix(path).to_ascii_lowercase();

View File

@@ -998,13 +998,7 @@ impl Compiler {
return Ok(result); return Ok(result);
} }
} }
Err(e) Err(e) if !self.alias_map.is_empty() && !self.alias_fallback_to_raw => {
if self
.alias_registry
.as_ref()
.is_some_and(|r| !r.alias_map().is_empty())
&& !self.alias_fallback_to_raw =>
{
return Err(e); return Err(e);
} }
_ => {} _ => {}

View File

@@ -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 crate::value::Object; use alloc::collections::BTreeMap;
use alloc::format; use alloc::format;
use alloc::string::ToString as _; use alloc::string::ToString as _;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -692,18 +692,13 @@ impl Compiler {
field_path: &str, field_path: &str,
span: &crate::lexer::Span, span: &crate::lexer::Span,
) -> Result<()> { ) -> Result<()> {
let modifiable_map = match &self.alias_registry { if self.alias_modifiable.is_empty() {
Some(reg) => reg.alias_modifiable_map(),
None => return Ok(()),
};
if modifiable_map.is_empty() {
return Ok(()); return Ok(());
} }
let lc = field_path.to_lowercase(); let lc = field_path.to_lowercase();
if let Some(&modifiable) = modifiable_map.get(&lc) { if let Some(&modifiable) = self.alias_modifiable.get(&lc) {
if !modifiable { if !modifiable {
bail!(span.error(&format!( bail!(span.error(&format!(
"alias '{}' is not modifiable (defaultMetadata.attributes != 'Modifiable')", "alias '{}' is not modifiable (defaultMetadata.attributes != 'Modifiable')",
@@ -808,41 +803,26 @@ fn unescape_arm_literal(s: &str) -> alloc::string::String {
/// 1. Build a template `BTreeMap` with `Value::Undefined` placeholders. /// 1. Build a template `BTreeMap` with `Value::Undefined` placeholders.
/// 2. Sort keys by their literal value (BTreeMap order). /// 2. Sort keys by their literal value (BTreeMap order).
/// 3. Emit `ObjectCreate`. /// 3. Emit `ObjectCreate`.
#[allow(clippy::indexing_slicing)]
pub(super) fn build_object_from_keys( pub(super) fn build_object_from_keys(
compiler: &mut Compiler, compiler: &mut Compiler,
mut keys: Vec<(u16, u8)>, mut keys: Vec<(u16, u8)>,
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 = Object::new(); let mut template = BTreeMap::new();
for &(key_idx, _) in &keys { for &(key_idx, _) in &keys {
// key_idx was returned by `add_literal_u16` in the calling code, // SAFETY: key_idx was just returned by `add_literal_u16`, so the
// so it is always in bounds. We use `.get()` + `?` instead of // index is guaranteed to be in bounds.
// direct indexing to satisfy the crate-wide `deny(indexing_slicing)`. let key_val = compiler.program.literals[usize::from(key_idx)].clone();
let key_val = compiler
.program
.literals
.get(usize::from(key_idx))
.ok_or_else(|| {
anyhow!(
"internal error in build_object_from_keys: \
literal index {} out of bounds (literals len = {})",
key_idx,
compiler.program.literals.len()
)
})?
.clone();
template.insert(key_val, Value::Undefined); template.insert(key_val, Value::Undefined);
} }
let template_idx = compiler.add_literal_u16(Value::Object(crate::Rc::new(template)))?; let template_idx = compiler.add_literal_u16(Value::Object(crate::Rc::new(template)))?;
// Sort keys by literal value (BTreeMap order). All indices were // Sort keys by literal value (BTreeMap order).
// validated in the loop above (which returns Err for out-of-bounds),
// so `.get()` always returns `Some` here — `None` is unreachable.
keys.sort_by(|a, b| { keys.sort_by(|a, b| {
let a_val = compiler.program.literals.get(usize::from(a.0)); compiler.program.literals[usize::from(a.0)]
let b_val = compiler.program.literals.get(usize::from(b.0)); .cmp(&compiler.program.literals[usize::from(b.0)])
a_val.cmp(&b_val)
}); });
let dest = compiler.alloc_register()?; let dest = compiler.alloc_register()?;

View File

@@ -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: &alloc::collections::BTreeSet<String>, observed: &BTreeSet<String>,
) { ) {
if !observed.is_empty() { if !observed.is_empty() {
let set: BTreeSet<Value> = observed let set: BTreeSet<Value> = observed

View File

@@ -30,11 +30,11 @@ mod metadata;
mod template_dispatch; mod template_dispatch;
mod utils; mod utils;
use alloc::string::ToString as _; use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _};
use anyhow::Result; use anyhow::Result;
use crate::languages::azure_policy::aliases::AliasRegistry;
use crate::languages::azure_policy::ast::{PolicyDefinition, PolicyRule}; use crate::languages::azure_policy::ast::{PolicyDefinition, PolicyRule};
use crate::rvm::program::Program; use crate::rvm::program::Program;
use crate::{Rc, Value}; use crate::{Rc, Value};
@@ -69,14 +69,17 @@ pub fn compile_policy_rule(rule: &PolicyRule) -> Result<Rc<Program>> {
/// Compile a parsed Azure Policy rule with alias resolution. /// Compile a parsed Azure Policy rule with alias resolution.
/// ///
/// The registry provides alias-to-short-name resolution and modifiability /// The `alias_map` maps lowercase fully-qualified alias names to their short
/// data. Pass it as an `Rc` to avoid cloning the internal alias maps. /// names. Obtain it from
/// [`AliasRegistry::alias_map()`](crate::languages::azure_policy::aliases::AliasRegistry::alias_map).
pub fn compile_policy_rule_with_aliases( pub fn compile_policy_rule_with_aliases(
rule: &PolicyRule, rule: &PolicyRule,
registry: Rc<AliasRegistry>, alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
) -> Result<Rc<Program>> { ) -> Result<Rc<Program>> {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
compiler.alias_registry = Some(registry); compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
init_effect_annotation(&mut compiler, rule); init_effect_annotation(&mut compiler, rule);
compiler.compile(rule) compiler.compile(rule)
} }
@@ -97,10 +100,12 @@ pub fn compile_policy_definition(defn: &PolicyDefinition) -> Result<Rc<Program>>
/// Compile a parsed Azure Policy definition with alias resolution. /// Compile a parsed Azure Policy definition with alias resolution.
pub fn compile_policy_definition_with_aliases( pub fn compile_policy_definition_with_aliases(
defn: &PolicyDefinition, defn: &PolicyDefinition,
registry: Rc<AliasRegistry>, alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
) -> Result<Rc<Program>> { ) -> Result<Rc<Program>> {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
compiler.alias_registry = Some(registry); compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?); compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?);
compiler.populate_definition_metadata(defn); compiler.populate_definition_metadata(defn);
init_effect_annotation(&mut compiler, &defn.policy_rule); init_effect_annotation(&mut compiler, &defn.policy_rule);
@@ -114,11 +119,13 @@ pub fn compile_policy_definition_with_aliases(
/// a known alias are silently treated as raw property paths. /// a known alias are silently treated as raw property paths.
pub fn compile_policy_definition_with_aliases_opts( pub fn compile_policy_definition_with_aliases_opts(
defn: &PolicyDefinition, defn: &PolicyDefinition,
registry: Rc<AliasRegistry>, alias_map: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
alias_fallback_to_raw: bool, alias_fallback_to_raw: bool,
) -> Result<Rc<Program>> { ) -> Result<Rc<Program>> {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
compiler.alias_registry = Some(registry); compiler.alias_map = alias_map;
compiler.alias_modifiable = alias_modifiable;
compiler.alias_fallback_to_raw = alias_fallback_to_raw; compiler.alias_fallback_to_raw = alias_fallback_to_raw;
compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?); compiler.parameter_defaults = Some(build_parameter_defaults(&defn.parameters)?);
compiler.populate_definition_metadata(defn); compiler.populate_definition_metadata(defn);

View File

@@ -264,17 +264,18 @@ impl<'source> Parser<'source> {
"metadata" => { "metadata" => {
*metadata = Some(self.parse_json_value()?); *metadata = Some(self.parse_json_value()?);
} }
"parameters" if self.token_text() == "{" => {
// Parameters must be a JSON object; if not, push to extra.
*parameters = self.parse_parameter_definitions()?;
}
"parameters" => { "parameters" => {
let value = self.parse_json_value()?; // Parameters must be a JSON object; if not, push to extra.
extra.push(ObjectEntry { if self.token_text() == "{" {
key_span, *parameters = self.parse_parameter_definitions()?;
key: key.into(), } else {
value, let value = self.parse_json_value()?;
}); extra.push(ObjectEntry {
key_span,
key: key.into(),
value,
});
}
} }
"policyrule" => { "policyrule" => {
// Parse the policyRule directly from the token stream! // Parse the policyRule directly from the token stream!

View File

@@ -63,14 +63,6 @@ pub enum CompilerError {
#[error("Invalid function expression with package")] #[error("Invalid function expression with package")]
InvalidFunctionExpressionWithPackage, InvalidFunctionExpressionWithPackage,
#[error("partial object rules with constant keys are not yet supported by the RVM compiler")]
PartialObjectConstantKeyUnsupported,
#[error(
"partial object rules with nested bracket keys are not yet supported by the RVM compiler"
)]
PartialObjectNestedKeyUnsupported,
#[error("Compilation error: {message}")] #[error("Compilation error: {message}")]
General { message: String }, General { message: String },
} }

View File

@@ -11,9 +11,8 @@ 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::BTreeSet; use alloc::collections::{BTreeMap, 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.
@@ -44,7 +43,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<Object>>() .collect::<Option<BTreeMap<_, _>>>()
.map(|m| Value::Object(Rc::new(m))), .map(|m| Value::Object(Rc::new(m))),
_ => None, _ => None,
} }
@@ -118,7 +117,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<Object> = fields let all_const: Option<BTreeMap<_, _>> = 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();
@@ -167,7 +166,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 = Object::new(); let mut template_obj = BTreeMap::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);
} }

View File

@@ -59,7 +59,7 @@ impl<'a> Compiler<'a> {
crate::ast::Expr::RefBrack { .. } if assign.is_some() => { crate::ast::Expr::RefBrack { .. } if assign.is_some() => {
RuleType::PartialObject RuleType::PartialObject
} }
crate::ast::Expr::RefBrack { .. } => RuleType::PartialObject, crate::ast::Expr::RefBrack { .. } => RuleType::PartialSet,
_ => RuleType::Complete, _ => RuleType::Complete,
}, },
_ => RuleType::Complete, _ => RuleType::Complete,
@@ -88,54 +88,6 @@ impl<'a> Compiler<'a> {
}) })
} }
fn validate_partial_object_shape(&self, refr: &ExprRef) -> Result<()> {
let Expr::RefBrack {
refr: prefix,
index,
..
} = refr.as_ref()
else {
return Ok(());
};
if Self::has_unsupported_bracket_prefix(prefix) {
return Err(CompilerError::PartialObjectNestedKeyUnsupported.at(refr.span()));
}
if Self::is_simple_literal(index) {
return Err(CompilerError::PartialObjectConstantKeyUnsupported.at(index.span()));
}
Ok(())
}
fn has_unsupported_bracket_prefix(expr: &ExprRef) -> bool {
match expr.as_ref() {
Expr::RefBrack { refr, index, .. } => {
!Self::is_string_literal(index) || Self::has_unsupported_bracket_prefix(refr)
}
Expr::RefDot { refr, .. } => Self::has_unsupported_bracket_prefix(refr),
_ => false,
}
}
fn is_string_literal(expr: &ExprRef) -> bool {
matches!(expr.as_ref(), Expr::String { .. } | Expr::RawString { .. })
}
fn is_simple_literal(expr: &ExprRef) -> bool {
match expr.as_ref() {
Expr::String { .. }
| Expr::RawString { .. }
| Expr::Number { .. }
| Expr::Bool { .. }
| Expr::Null { .. } => true,
// Unary expressions like `-1` are constant literals too.
Expr::UnaryExpr { expr, .. } => Self::is_simple_literal(expr),
_ => false,
}
}
pub(super) fn get_or_assign_rule_index(&mut self, rule_path: &str) -> Result<u16> { pub(super) fn get_or_assign_rule_index(&mut self, rule_path: &str) -> Result<u16> {
if let Some(&index) = self.rule_index_map.get(rule_path) { if let Some(&index) = self.rule_index_map.get(rule_path) {
return Ok(index); return Ok(index);
@@ -393,10 +345,6 @@ impl<'a> Compiler<'a> {
let (key_expr, value_expr) = match head { let (key_expr, value_expr) = match head {
RuleHead::Compr { refr, assign, .. } => { RuleHead::Compr { refr, assign, .. } => {
if rule_type == RuleType::PartialObject {
self.validate_partial_object_shape(refr)?;
}
self.rule_definition_function_params[rule_index as usize].push(None); self.rule_definition_function_params[rule_index as usize].push(None);
self.rule_definition_destructuring_patterns[rule_index as usize] self.rule_definition_destructuring_patterns[rule_index as usize]
.push(None); .push(None);

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;
pub mod value; mod value;
#[cfg(feature = "azure_policy")] #[cfg(feature = "azure_policy")]
pub use { pub use {

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, BTreeSet}; use alloc::collections::BTreeMap;
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 = Object::new(); let mut obj = BTreeMap::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 = Object::new(); let mut annotations_obj = BTreeMap::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 = BTreeSet::new(); let mut bset = alloc::collections::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 = Object::new(); let mut obj = BTreeMap::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,6 +257,7 @@ 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).

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::BTreeSet; use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format; use alloc::format;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
@@ -11,7 +11,6 @@ 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;
@@ -133,7 +132,7 @@ impl<'a> Serialize for BinarySetRef<'a> {
} }
} }
struct BinaryObjectRef<'a>(&'a Object); struct BinaryObjectRef<'a>(&'a BTreeMap<Value, Value>);
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>
@@ -141,7 +140,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_sorted() { for (key, value) in self.0.iter() {
seq.serialize_element(&BinaryEntryRef(key, value))?; seq.serialize_element(&BinaryEntryRef(key, value))?;
} }
seq.end() seq.end()
@@ -262,11 +261,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 = Object::new(); let mut map = BTreeMap::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::Object(crate::Rc::new(map)))) Ok(BinaryValue(Value::from(map)))
} }
(BinaryVariant::Undefined, variant) => { (BinaryVariant::Undefined, variant) => {
variant.unit_variant()?; variant.unit_variant()?;

View File

@@ -6,6 +6,8 @@
// 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;
@@ -30,9 +32,8 @@ 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: alloc::collections::BTreeSet<Value> = let diff: BTreeSet<Value> = left.difference(right).cloned().collect();
left.difference(right).cloned().collect(); Ok(Value::from_set(diff))
Ok(Value::from(diff))
} }
_ => Err(VmError::InvalidSubtraction { _ => Err(VmError::InvalidSubtraction {
left: a.clone(), left: a.clone(),

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(Object::new())), ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::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 mut iteration_state = if auto_iterate { let 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,9 +53,11 @@ impl RegoVM {
if obj.is_empty() { if obj.is_empty() {
None None
} else { } else {
// O(1) cursor over shared Rc<Object>. 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) => {
@@ -77,7 +79,7 @@ impl RegoVM {
None None
}; };
let has_iteration = if let Some(state) = iteration_state.as_mut() { let has_iteration = if let Some(state) = iteration_state.as_ref() {
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
@@ -121,12 +123,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(Object::new())), ComprehensionMode::Object => Value::Object(Rc::new(BTreeMap::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 mut iteration_state = if auto_iterate { let 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) => {
@@ -140,8 +142,11 @@ impl RegoVM {
if obj.is_empty() { if obj.is_empty() {
None None
} else { } else {
let cursor = obj.cursor(); Some(IterationState::Object {
Some(IterationState::Object { obj, cursor }) obj,
current_key: None,
first_iteration: true,
})
} }
} }
Value::Set(set) => { Value::Set(set) => {
@@ -163,7 +168,7 @@ impl RegoVM {
None None
}; };
let has_iteration = if let Some(state) = iteration_state.as_mut() { let has_iteration = if let Some(state) = iteration_state.as_ref() {
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
@@ -250,21 +255,6 @@ 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)?;
@@ -302,16 +292,29 @@ 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() {
// Set's `Bound::Excluded(current_item)` resume scheme needs the match *iter_state {
// pre-mutation snapshot taken at the top of this function. IterationState::Object {
// Object uses a self-advancing cursor and needs no snapshot. ref mut current_key,
if let IterationState::Set { ..
ref mut current_item, } => {
.. let tracked_key =
} = *iter_state if comprehension_context.key_reg != comprehension_context.value_reg {
{ self.get_register(comprehension_context.key_reg)?.clone()
*current_item = set_resume_snapshot; } else {
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,
@@ -356,7 +359,8 @@ impl RegoVM {
result_reg_idx, result_reg_idx,
key_reg_idx, key_reg_idx,
value_reg_idx, value_reg_idx,
iter_is_set, iteration_key,
iteration_value,
) = { ) = {
let frame = let frame =
self.execution_stack self.execution_stack
@@ -378,8 +382,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 iter_is_set = let iteration_key = self.get_register(context.key_reg)?.clone();
matches!(context.iteration_state, Some(IterationState::Set { .. })); let iteration_value = self.get_register(context.value_reg)?.clone();
( (
value_to_add, value_to_add,
@@ -388,7 +392,8 @@ impl RegoVM {
result_reg_idx, result_reg_idx,
context.key_reg, context.key_reg,
context.value_reg, context.value_reg,
iter_is_set, iteration_key,
iteration_value,
) )
} else { } else {
return Err(VmError::InvalidIteration { return Err(VmError::InvalidIteration {
@@ -398,18 +403,6 @@ 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)?;
@@ -457,13 +450,27 @@ 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() {
if let IterationState::Set { match *iter_state {
ref mut current_item, IterationState::Object {
.. ref mut current_key,
} = *iter_state ..
{ } => {
*current_item = set_resume_snapshot; let tracked_key = if context.key_reg != context.value_reg {
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();
} }
@@ -480,21 +487,8 @@ impl RegoVM {
} }
}; };
if let Some(mut state) = iteration_state_snapshot { if let Some(state) = iteration_state_snapshot.as_ref() {
let has_next = self.setup_next_iteration(&mut state, key_reg_idx, value_reg_idx)?; let has_next = self.setup_next_iteration(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) {
@@ -534,6 +528,7 @@ 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 {
@@ -559,16 +554,11 @@ 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() {
// Snapshot the current value into Set's `current_item` so the self.capture_comprehension_iteration_position(
// next iteration can resume from `Bound::Excluded(current)`. iter_state,
// Object uses a self-advancing cursor and needs no snapshot here. context.key_reg,
if let IterationState::Set { context.value_reg,
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)?;
@@ -585,10 +575,37 @@ 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 {

View File

@@ -3,9 +3,8 @@
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::BTreeSet; use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec; use alloc::vec::Vec;
/// Loop execution context for managing iteration state /// Loop execution context for managing iteration state
@@ -25,18 +24,7 @@ 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 {
@@ -44,8 +32,9 @@ pub enum IterationState {
index: usize, index: usize,
}, },
Object { Object {
obj: Rc<Object>, obj: Rc<BTreeMap<Value, Value>>,
cursor: ObjectCursor, current_key: Option<Value>,
first_iteration: bool,
}, },
Set { Set {
items: Rc<BTreeSet<Value>>, items: Rc<BTreeSet<Value>>,
@@ -65,21 +54,13 @@ 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);
} }
// For Object the cursor advances inside `setup_next_iteration` Self::Object {
// when it pulls the next item via `Object::next`, so `advance` ref mut first_iteration,
// is a no-op for the cursor-backed Object variant. ..
Self::Object { .. } => {} }
Self::Set { | Self::Set {
ref mut first_iteration, ref mut first_iteration,
.. ..
} => { } => {
@@ -88,12 +69,6 @@ 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;
} }
} }
@@ -132,71 +107,3 @@ 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)));
}
}

View File

@@ -4,6 +4,7 @@
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;
@@ -669,7 +670,7 @@ impl RegoVM {
} }
} }
SetNew { dest } => { SetNew { dest } => {
let empty_set = Value::new_set(); let empty_set = Value::Set(crate::Rc::new(BTreeSet::new()));
self.set_register(dest, empty_set)?; self.set_register(dest, empty_set)?;
Ok(InstructionOutcome::Continue) Ok(InstructionOutcome::Continue)
} }
@@ -706,7 +707,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 = alloc::collections::BTreeSet::new(); let mut set = 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());
} }

View File

@@ -295,13 +295,6 @@ 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 },
} }

View File

@@ -117,10 +117,6 @@ 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 {
@@ -193,9 +189,6 @@ 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) {
@@ -208,10 +201,6 @@ 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,
@@ -300,9 +289,6 @@ 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| {

View File

@@ -3,7 +3,6 @@
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};
@@ -90,13 +89,13 @@ impl RegoVM {
) -> Result<()> { ) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?; self.set_register(params.result_reg, Value::Bool(false))?;
let mut iteration_state = match self.resolve_iteration_state(mode, &params)? { let 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(&mut iteration_state, params.key_reg, params.value_reg)?; self.setup_next_iteration(&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(());
@@ -156,10 +155,15 @@ impl RegoVM {
LoopAction::Continue => {} LoopAction::Continue => {}
} }
// Snapshot the current value for Set so its next iteration can resume if let &mut IterationState::Object {
// from `Bound::Excluded(current)`. Object uses a cursor and advances ref mut current_key,
// inside `setup_next_iteration` itself. ..
if let &mut IterationState::Set { } = &mut loop_ctx.iteration_state
{
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
@@ -169,7 +173,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(
&mut loop_ctx.iteration_state, &loop_ctx.iteration_state,
loop_ctx.key_reg, loop_ctx.key_reg,
loop_ctx.value_reg, loop_ctx.value_reg,
)?; )?;
@@ -207,13 +211,13 @@ impl RegoVM {
) -> Result<()> { ) -> Result<()> {
self.set_register(params.result_reg, Value::Bool(false))?; self.set_register(params.result_reg, Value::Bool(false))?;
let mut iteration_state = match self.resolve_iteration_state(mode, &params)? { let 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(&mut iteration_state, params.key_reg, params.value_reg)?; self.setup_next_iteration(&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(());
@@ -312,14 +316,7 @@ impl RegoVM {
Ok(()) Ok(())
} }
LoopAction::Continue => { LoopAction::Continue => {
let ( let (mode, success_count, total_iterations, key_reg, value_reg, iteration_state) = {
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
@@ -337,6 +334,11 @@ 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
@@ -347,16 +349,20 @@ impl RegoVM {
&mut FrameKind::Loop { &mut FrameKind::Loop {
ref mut context, .. ref mut context, ..
} => { } => {
// Snapshot the current value for Set so its next if let &mut IterationState::Object {
// iteration can resume from `Bound::Excluded(current)`. ref mut current_key,
// Object uses a cursor and advances inside ..
// `setup_next_iteration` itself. } = &mut context.iteration_state
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); *current_item = Some(value_value.clone());
} }
context.iteration_state.advance(); context.iteration_state.advance();
@@ -375,21 +381,7 @@ impl RegoVM {
} }
}; };
let has_next = let has_next = self.setup_next_iteration(&iteration_state, key_reg, value_reg)?;
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() {
@@ -467,14 +459,10 @@ 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: Rc::clone(obj), obj: obj.clone(),
cursor, current_key: None,
first_iteration: true,
})) }))
} }
} }
@@ -524,7 +512,7 @@ impl RegoVM {
pub(super) fn setup_next_iteration( pub(super) fn setup_next_iteration(
&mut self, &mut self,
state: &mut IterationState, state: &IterationState,
key_reg: u8, key_reg: u8,
value_reg: u8, value_reg: u8,
) -> Result<bool> { ) -> Result<bool> {
@@ -550,19 +538,33 @@ impl RegoVM {
} }
IterationState::Object { IterationState::Object {
ref obj, ref obj,
ref mut cursor, ref current_key,
ref first_iteration,
} => { } => {
// Object iterates via a resumable cursor on the shared if *first_iteration {
// `Rc<Object>`; `next` both yields the current entry and if let Some((key, value)) = obj.iter().next() {
// advances the cursor. No explicit `current_key` snapshot is if key_reg != value_reg {
// needed — see the doc on `IterationState`. self.set_register(key_reg, key.clone())?;
if let Some((key, value)) = obj.next(cursor) { }
let value = value.clone(); self.set_register(value_reg, value.clone())?;
if key_reg != value_reg { Ok(true)
self.set_register(key_reg, key.clone())?; } else {
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)
} }

View File

@@ -277,19 +277,6 @@ 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 {
@@ -844,9 +831,6 @@ 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 });
} }

View File

@@ -34,139 +34,6 @@ 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

View File

@@ -569,7 +569,7 @@ impl Analyzer {
} }
Ok(false) Ok(false)
} }
Expr::Array { .. } | Expr::Object { .. } => Ok(true), Array { .. } | 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
Expr::Array { .. } | Expr::Object { .. } => Ok(true), Array { .. } | 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
Expr::Array { .. } | Expr::Object { .. } => Ok(true), Array { .. } | Object { .. } => Ok(true),
_ => { _ => {
non_vars.push(e.clone()); non_vars.push(e.clone());
Ok(false) Ok(false)

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::new_object())); assert_eq!(default, &Some(Value::Object(Rc::new(BTreeMap::new()))));
} }
_ => panic!("Expected Type::Object"), _ => panic!("Expected Type::Object"),
} }
@@ -1778,11 +1778,8 @@ 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.get(&Value::from("1")).expect("1"), Value::from("one")); assert_eq!(obj[&Value::from("1")], Value::from("one"));
assert_eq!( assert_eq!(obj[&Value::from("true")], Value::from("bool"));
*obj.get(&Value::from("true")).expect("true"),
Value::from("bool")
);
} }
_ => panic!("Expected object in enum values"), _ => panic!("Expected object in enum values"),
}, },
@@ -1805,21 +1802,18 @@ 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.get(&Value::from("a")).expect("a"); let a = &obj[&Value::from("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.get(&Value::from("b")).expect("b"); let b = &inner[&Value::from("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!( assert_eq!(cobj[&Value::from("c")], Value::Null);
*cobj.get(&Value::from("c")).expect("c"),
Value::Null
);
} }
_ => panic!("Expected object for 'c'"), _ => panic!("Expected object for 'c'"),
} }
@@ -1892,11 +1886,8 @@ 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!( assert_eq!(obj[&Value::from("foo")], Value::from("bar"));
*obj.get(&Value::from("foo")).expect("foo"), assert_eq!(obj[&Value::from("baz")], Value::from(1));
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"),
}, },
@@ -1949,13 +1940,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.get(&Value::from("a")).expect("a"); let a = &obj[&Value::from("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.get(&Value::from("b")).expect("b"); let b = &inner[&Value::from("b")];
match b { match b {
Value::Array(barr) => { Value::Array(barr) => {
assert_eq!(barr[0], Value::Null); assert_eq!(barr[0], Value::Null);

View File

@@ -7,7 +7,6 @@
use crate::{ use crate::{
schema::{error::ValidationError, Schema, Type}, schema::{error::ValidationError, Schema, Type},
value::Object,
*, *,
}; };
use alloc::collections::BTreeMap; use alloc::collections::BTreeMap;
@@ -538,7 +537,7 @@ impl SchemaValidator {
} }
fn validate_discriminated_subobject_with_base( fn validate_discriminated_subobject_with_base(
object_value: &Object, object_value: &BTreeMap<Value, Value>,
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>,
@@ -654,7 +653,7 @@ impl SchemaValidator {
} }
fn validate_subobject( fn validate_subobject(
object_value: &Object, object_value: &BTreeMap<Value, Value>,
subobject: &crate::schema::Subobject, subobject: &crate::schema::Subobject,
path: &str, path: &str,
) -> Result<(), ValidationError> { ) -> Result<(), ValidationError> {

View File

@@ -88,7 +88,7 @@ fn analyze_file(regos: &[String], expected_scopes: &[Scope]) -> Result<()> {
} }
} }
scopes.sort_by_key(|a| a.0.span.line); scopes.sort_by(|a, b| a.0.span.line.cmp(&b.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.")

View File

@@ -11,18 +11,6 @@
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};
@@ -35,7 +23,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::Serializer; use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::*; use crate::*;
@@ -75,7 +63,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<Object>), Object(Rc<BTreeMap<Value, Value>>),
/// Undefined value. /// Undefined value.
/// Used to indicate the absence of a value. /// Used to indicate the absence of a value.
@@ -98,15 +86,26 @@ 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),
// Delegate to the Object/Set serializers — single canonical path, Value::Object(fields) => {
// handles non-string-key stringification internally. let mut map = serializer.serialize_map(Some(fields.len()))?;
Value::Object(fields) => fields.serialize(serializer), for (k, v) in fields.iter() {
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),
@@ -346,7 +345,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.get(&Value::from("name")).expect("missing name"), &Value::from("regorus")); /// assert_eq!(obj[&Value::from("name")], Value::from("regorus"));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -801,7 +800,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(Object::from(s))) Value::Object(Rc::new(s))
} }
} }
@@ -1280,16 +1279,16 @@ impl Value {
} }
} }
/// Cast value to [`&Object`] if [`Value::Object`]. /// Cast value to [`& BTreeMap<Value, Value>`] if [`Value::Object`].
/// ``` /// ```
/// # use regorus::*; /// # use regorus::*;
/// # use regorus::value::Object; /// # use std::collections::BTreeMap;
/// # 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::<Object>(), /// .collect::<BTreeMap<Value, Value>>(),
/// ); /// );
/// assert_eq!( /// assert_eq!(
/// v.as_object()?.iter().next(), /// v.as_object()?.iter().next(),
@@ -1297,28 +1296,28 @@ impl Value {
/// ); /// );
/// # Ok(()) /// # Ok(())
/// # } /// # }
pub fn as_object(&self) -> Result<&Object> { pub fn as_object(&self) -> Result<&BTreeMap<Value, Value>> {
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 Object`] if [`Value::Object`]. /// Cast value to [`&mut BTreeMap<Value, Value>`] if [`Value::Object`].
/// ``` /// ```
/// # use regorus::*; /// # use regorus::*;
/// # use regorus::value::Object; /// # use std::collections::BTreeMap;
/// # 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::<Object>(), /// .collect::<BTreeMap<Value, Value>>(),
/// ); /// );
/// 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 Object> { pub fn as_object_mut(&mut self) -> Result<&mut BTreeMap<Value, Value>> {
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")),

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