Compare commits

..

20 Commits

Author SHA1 Message Date
Jay Lorch
f44e019f41 Spec for Number as Ord 2026-03-19 15:52:41 -07:00
Jay Lorch
2b4ab8e12d Simpler spec for Number::eq 2026-03-19 14:48:03 -07:00
Jay Lorch
41c2d502fa Simplify Number::eq spec 2026-03-19 13:58:10 -07:00
Jay Lorch
85e58c6f6c Simplify float comparisons by assuming IEEE 2026-03-19 11:52:35 -07:00
Jay Lorch
9e18ded98e Simplify float spec by avoiding partial_cmp_spec 2026-03-19 11:33:30 -07:00
Jay Lorch
2e454f2708 Simplify float specs by assuming determinism 2026-03-19 10:49:59 -07:00
Jay Lorch
d107c1b647 Use float specs in latest Verus 2026-03-18 18:21:10 -07:00
Jay Lorch
462e39f2ad Switch to attribute syntax 2026-03-12 18:31:02 -07:00
Jay Lorch
7e1afe0e86 More specs for Number 2026-03-11 17:43:20 -07:00
Jay Lorch
faa3efb8b8 Update Cargo.toml to use public vstd crate 2026-03-04 10:05:58 -08:00
Jay Lorch
100be610db Don't shut off so much of bigint when not verifying 2026-03-03 17:00:30 -08:00
Jay Lorch
586d631f09 Got cargo build working 2026-03-03 16:46:09 -08:00
Jay Lorch
5819992d17 More progress toward cargo build 2026-03-03 16:44:31 -08:00
Jay Lorch
90894aa8e1 Progress to cargo build success 2026-03-03 16:35:05 -08:00
Jay Lorch
1f2a2ecb41 More specs for Number 2026-03-03 16:20:59 -08:00
Jay Lorch
ba987998c4 More Number proofs 2026-03-03 15:58:36 -08:00
Jay Lorch
c3813c8876 Avoid some build errors 2026-03-03 15:16:35 -08:00
Jay Lorch
6ab452989f Proofs about some float operations 2026-03-03 14:43:45 -08:00
Jay Lorch
4865364b48 Verus specs for BigNum, Number 2026-02-27 15:52:00 -08:00
Jay Lorch
156772c523 Add Verus dependencies 2026-02-17 13:25:11 -08:00
335 changed files with 2339 additions and 34747 deletions

104
.github/dependabot.yml vendored
View File

@@ -7,26 +7,12 @@
version: 2
updates:
# All Rust/Cargo directories are grouped into a single entry so that
# when a dependency is updated, Dependabot bumps it across the root
# workspace AND every binding, preventing version skew.
- package-ecosystem: "cargo"
directories:
- "/"
- "/bindings/ffi"
- "/bindings/java"
- "/bindings/python"
- "/bindings/ruby"
- "/bindings/wasm"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
# Bundle all Cargo dependency updates into a single PR. Without this,
# dependabot creates a separate PR per directory for the same dependency,
# and each individual PR fails to build due to version skew.
rust-dependencies:
per-dependency:
patterns:
- "*"
# Ignore vendored mimalloc crates; updates are managed manually.
@@ -34,12 +20,82 @@ updates:
- dependency-name: "regorus-mimalloc"
- dependency-name: "regorus-mimalloc-sys"
- package-ecosystem: "cargo"
directory: "/bindings/ffi"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/bindings/java"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/bindings/python"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/bindings/ruby"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/bindings/ruby/ext/regorusrb"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/bindings/wasm"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/tests/ensure_no_std"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/xtask"
schedule:
interval: "weekly"
groups:
per-dependency:
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/bindings/go"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
@@ -49,8 +105,6 @@ updates:
directory: "/bindings/java"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
@@ -60,8 +114,6 @@ updates:
directory: "/bindings/csharp"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
@@ -71,8 +123,6 @@ updates:
directory: "/bindings/python"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
@@ -82,8 +132,6 @@ updates:
directory: "/bindings/ruby"
schedule:
interval: "weekly"
commit-message:
prefix: "build(deps)"
groups:
per-dependency:
patterns:
@@ -93,9 +141,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci(deps)"
groups:
github-actions:
per-dependency:
patterns:
- "*"

View File

@@ -69,7 +69,7 @@ jobs:
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
@@ -99,13 +99,13 @@ jobs:
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: '1.21'
- name: Setup .NET
if: matrix.language == 'csharp'
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
global-json-file: ./bindings/csharp/global.json
@@ -115,12 +115,12 @@ jobs:
- name: Setup Node.js
if: matrix.language == 'javascript-typescript'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '18'
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -141,7 +141,7 @@ jobs:
- name: Setup Ruby
if: matrix.language == 'rust' && contains(matrix.working-directory, 'ruby')
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1.288.0
with:
ruby-version: '3.4.2'
bundler-cache: true
@@ -188,6 +188,6 @@ jobs:
run: cargo xtask build-wasm --release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
with:
category: "/language:${{matrix.language}}"

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -56,7 +56,7 @@ jobs:
- run: cargo ${{ matrix.build_cmd || 'build' }} --release --frozen --target ${{ matrix.target }}${{ matrix.glibc && format('.{0}', matrix.glibc) || '' }} --manifest-path ./bindings/java/Cargo.toml
- run: mkdir -p native/${{ matrix.target }}
- run: mv target/${{ matrix.target }}/release/*.${{ matrix.extension }} ./native/${{ matrix.target }}/
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: native-libraries-${{ matrix.target }}
path: native/
@@ -76,14 +76,14 @@ jobs:
server-id: ossrh
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: native-libraries-*
merge-multiple: true
path: ./bindings/java/native/
- run: mvn package
working-directory: ./bindings/java
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: built-jars
path: ./bindings/java/target/regorus-java-*.jar

View File

@@ -34,14 +34,14 @@ jobs:
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: wheels-linux-${{ matrix.target }}
path: dist
@@ -67,13 +67,13 @@ jobs:
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --frozen --strip
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: wheels-windows-${{ matrix.target }}
path: dist
@@ -98,13 +98,13 @@ jobs:
working-directory: bindings/python
- name: Build wheels
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.43.0
with:
target: ${{ matrix.target }}
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: wheels-macos-${{ matrix.host.target }}
path: dist
@@ -116,13 +116,13 @@ jobs:
# if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos]
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: wheels-*
merge-multiple: true
path: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
uses: PyO3/maturin-action@b1bd829e37fef14c63f19162034228a2f3dc1021 # v1.43.0
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:

View File

@@ -19,7 +19,7 @@ jobs:
with:
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'

View File

@@ -23,7 +23,7 @@ jobs:
- name: Install Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Run release-plz
uses: MarcoIeni/release-plz-action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5.128
uses: MarcoIeni/release-plz-action@52440b50d383aa252927de395c8b2c1e0a9cf8e9 # v0.5.126
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View File

@@ -37,7 +37,7 @@ jobs:
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
@@ -49,10 +49,10 @@ jobs:
- name: Run rust-clippy
run: cargo xtask clippy --sarif rust-clippy-results.sarif
continue-on-error: true
- name: Upload analysis results to GitHub
if: ${{ hashFiles('rust-clippy-results.sarif') != '' }}
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.11
uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.11
with:
sarif_file: rust-clippy-results.sarif
wait-for-processing: true

View File

@@ -22,7 +22,7 @@ jobs:
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -46,7 +46,7 @@ jobs:
with:
targets: ${{ matrix.runtime.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
@@ -59,7 +59,7 @@ jobs:
run: cargo xtask build-ffi --release --target ${{ matrix.runtime.target }}
- name: Upload regorus ffi shared library
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: regorus-ffi-artifacts-${{ matrix.runtime.target }}
# Note: The full path of each artifact relative to . is preserved.
@@ -77,14 +77,14 @@ jobs:
with:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
global-json-file: ./bindings/csharp/global.json
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
@@ -92,7 +92,7 @@ jobs:
run: cargo fetch --locked
- name: Download regorus ffi shared libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: regorus-ffi-artifacts-*
merge-multiple: true
@@ -105,7 +105,7 @@ jobs:
run: cargo xtask build-csharp --release --clean --artifacts-dir ./bindings/csharp/Regorus/tmp/bindings/ffi/target --enforce-artifacts --repository-commit ${{ github.sha }} --include-symbols
- name: Upload Regorus nuget
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: regorus-nuget
path: |
@@ -137,14 +137,14 @@ jobs:
- uses: ./.github/actions/toolchains/rust
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
global-json-file: ./bindings/csharp/global.json
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
@@ -152,7 +152,7 @@ jobs:
run: cargo fetch --locked
- name: Download regorus nuget
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: regorus-nuget
path: ./bindings/csharp/Regorus/bin/Release

View File

@@ -21,7 +21,7 @@ jobs:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -21,7 +21,7 @@ jobs:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
@@ -30,7 +30,7 @@ jobs:
- name: Fetch FFI crate dependencies
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
architecture: x64

View File

@@ -26,7 +26,7 @@ jobs:
distribution: "corretto"
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -25,7 +25,7 @@ jobs:
with:
targets: x86_64-unknown-linux-musl
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -25,7 +25,7 @@ jobs:
with:
targets: thumbv7m-none-eabi
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -30,7 +30,7 @@ jobs:
with:
targets: ${{ matrix.host.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
@@ -51,7 +51,7 @@ jobs:
run: cargo xtask build-python --release --target ${{ matrix.host.target }} --target-dir bindings/python/dist --frozen
- name: Upload wheel artefacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: regorus-wheel-${{ matrix.host.name }}
path: bindings/python/dist/regorus-*.whl
@@ -70,7 +70,7 @@ jobs:
fetch-depth: 0
- uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

View File

@@ -30,7 +30,7 @@ jobs:
working-directory: "bindings/ruby"
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies
@@ -33,7 +33,7 @@ jobs:
run: cargo fetch --locked --manifest-path bindings/wasm/Cargo.toml
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Rust toolchain
uses: ./.github/actions/toolchains/rust
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
shared-key: ${{ runner.os }}-regorus
- name: Fetch dependencies

6
.gitignore vendored
View File

@@ -25,12 +25,6 @@ bindings/ffi/regorus.ffi.hpp
bindings/*/target
# Temporary commit message files
.commit-msg.txt
# Local planning docs
docs/plans/
# C# build folders
**bin
**obj

View File

@@ -6,13 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Azure RBAC condition interpreter with builtin evaluation coverage and YAML test suite, including quantifier (ForAnyOfAnyValues/ForAllOfAllValues), datetime (DateTimeEquals), IP (IpInRange), GUID (GuidEquals), list (ListContains), and string (StringEquals) semantics.
- FFI surface for Azure RBAC condition evaluation (see bindings changelog for language-specific wrappers).
### Changed
- [**breaking**] Switch RVM binary serialization to postcard, bump the format to v4, and mark v1-3 loads as partial (recompile required).
## [0.9.1](https://github.com/microsoft/regorus/compare/regorus-v0.9.0...regorus-v0.9.1) - 2026-02-06
### Fixed

699
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,8 @@ default = ["full-opa", "arc", "rvm"]
arc = []
ast = []
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"]
azure-rbac = ["regex", "time", "net"]
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
azure-rbac = []
base64 = ["dep:data-encoding"]
base64url = ["dep:data-encoding"]
coverage = []
@@ -44,11 +44,10 @@ net = ["dep:ipnet"]
no_std = ["lazy_static/spin_no_std"]
opa-runtime = []
regex = ["dep:regex"]
cache = ["dep:lru"]
rvm = ["dep:postcard", "dep:indexmap"]
rvm = ["dep:bincode", "dep:indexmap"]
semver = ["dep:semver"]
allocator-memory-limits = ["std", "mimalloc", "mimalloc/allocator-memory-limits"]
std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot" ]
std = ["rand/std", "rand/std_rng", "serde_json/std", "msvc_spectre_libs" ]
time = ["dep:chrono", "dep:chrono-tz"]
uuid = ["dep:uuid"]
urlquery = ["dep:url"]
@@ -62,10 +61,11 @@ full-opa = [
"hex",
"http",
"jsonschema",
"allocator-memory-limits",
"mimalloc",
"net",
"opa-runtime",
"regex",
"cache",
"semver",
"std",
"time",
@@ -101,49 +101,45 @@ opa-testutil = []
rand = ["dep:rand"]
[dependencies]
anyhow = { version = "1.0.102", default-features = false }
anyhow = { version = "1.0.45", default-features = false }
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
hashbrown = { version = "0.16", default-features = false, features = ["default-hasher"], optional = true }
lazy_static = { version = "1.4.0", default-features = false }
thiserror = { version = "2.0", default-features = false }
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
num-bigint = { version = "0.4", default-features = false }
num-traits = { version = "0.2", default-features = false }
parking_lot = { version = "0.12", optional = true }
spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] }
spin = { version = "0.9.8", default-features = false, features = ["mutex", "spin_mutex"] }
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.11.1", optional = true, default-features = false }
semver = {version = "1.0.25", optional = true, default-features = false }
url = { version = "2.5.4", optional = true }
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.45.0", default-features = false, optional = true }
chrono = { version = "0.4.44", optional = true }
uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.30.0", default-features = false, optional = true }
chrono = { version = "0.4.40", optional = true }
chrono-tz = { version = "0.10.1", optional = true }
ipnet = { version = "2.12.0", optional = true, default-features = false }
icu_casemap = { version = "2.1", optional = true, default-features = false, features = ["compiled_data"] }
ipnet = { version = "2.11.0", optional = true, default-features = false }
serde_yaml = {version = "0.9.16", default-features = false, optional = true }
# Specify thread_rng for in order to use random_range
rand = { version = "0.10.0", default-features = false, features = ["thread_rng"], optional = true }
rand = { version = "0.9.0", default-features = false, features = ["thread_rng"], optional = true }
# Causes the project to link with the Spectre-mitigated CRT and libs.
msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true }
dashmap = { version = "6.1", default-features = false, optional = true }
lru = { version = "0.16", default-features = false, optional = true }
mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.6", optional = true }
# rvm related deps
indexmap = { version = "2.12.1", default-features = false, features = ["serde"], optional = true }
postcard = { version = "1.1.3", default-features = false, features = ["alloc"], optional = true }
bincode = { version = "2.0.1", default-features = false, features = ["alloc", "serde"], optional = true }
# Use Verus for verification
vstd = { version = "0.0.0-2026-03-17-2326" }
[dev-dependencies]
anyhow = "1.0.102"
anyhow = "1.0.45"
cfg-if = "1.0.0"
clap = { version = "4.5.53", features = ["derive"] }
prettydiff = { version = "0.9.0", default-features = false }
@@ -201,16 +197,6 @@ harness = false
name = "aci_benchmark"
harness = false
[[bench]]
name = "rvm_benchmark"
harness = false
required-features = ["rvm"]
[[bench]]
name = "normalization_benchmark"
harness = false
required-features = ["azure_policy"]
[[example]]
name="regorus"
harness=false

View File

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

View File

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

View File

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

View File

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

View File

@@ -131,15 +131,7 @@ namespace regorus {
Result get_coverage_report_pretty() {
return Result(regorus_engine_get_coverage_report_pretty(engine));
}
Result set_policy_length_config(RegorusPolicyLengthConfig config) {
return Result(regorus_engine_set_policy_length_config(engine, config));
}
Result clear_policy_length_config() {
return Result(regorus_engine_clear_policy_length_config(engine));
}
~Engine() {
regorus_engine_drop(engine);
}
@@ -158,14 +150,6 @@ namespace regorus {
Engine& operator=(const Engine&) = delete;
};
inline Result set_cache_config(RegorusCacheConfig config) {
return Result(regorus_set_cache_config(config));
}
inline Result clear_cache() {
return Result(regorus_clear_cache());
}
class CompiledPolicy {
public:
explicit CompiledPolicy(RegorusCompiledPolicy* p) : policy(p) {}

View File

@@ -10,6 +10,5 @@
<PackageVersion Include="Regorus" Version="$(RegorusPackageVersion)$(RegorusPackageVersionSuffix)" />
<PackageVersion Include="MSTest" Version="3.8.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="YamlDotNet" Version="13.7.0" />
</ItemGroup>
</Project>

View File

@@ -104,49 +104,3 @@ vm.SetInputJson(Input);
var result = vm.Execute();
Console.WriteLine($"allow: {result}");
```
## Azure RBAC Condition Evaluation
Evaluate Azure RBAC condition expressions directly with a JSON evaluation context:
```csharp
using Regorus;
const string Condition = "@Resource[owner] StringEquals 'alice'";
const string ContextJson = """
{
"principal": {
"id": "user-1",
"principal_type": "User",
"custom_security_attributes": {}
},
"resource": {
"id": "/subscriptions/s1",
"resource_type": "Microsoft.Storage/storageAccounts",
"scope": "/subscriptions/s1",
"attributes": {
"owner": "alice",
"confidential": true
}
},
"request": {
"action": "Microsoft.Storage/storageAccounts/read",
"data_action": null,
"attributes": {
"clientIP": "10.0.0.1"
}
},
"environment": {
"is_private_link": null,
"private_endpoint": null,
"subnet": null,
"utc_now": "2023-05-01T12:00:00Z"
},
"action": "Microsoft.Storage/storageAccounts/read",
"suboperation": null
}
""";
var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson);
Console.WriteLine($"RBAC condition allowed: {allowed}");
```

View File

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

View File

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

View File

@@ -20,14 +20,9 @@
<ItemGroup>
<PackageReference Include="MSTest" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Regorus" />
</ItemGroup>
<ItemGroup>
<None Include="../../../src/languages/azure_rbac/test_cases/*.yaml" Link="test_cases/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -34,17 +34,6 @@ namespace Regorus
CheckAndDropResult(Regorus.Internal.API.regorus_clear_fallback_execution_timer_config());
}
public static void SetCacheConfig(CacheConfig config)
{
var nativeConfig = config.ToNative();
CheckAndDropResult(Regorus.Internal.API.regorus_set_cache_config(nativeConfig));
}
public static void ClearCache()
{
CheckAndDropResult(Regorus.Internal.API.regorus_clear_cache());
}
private Engine(RegorusEngineHandle handle)
: base(handle, nameof(Engine))
{
@@ -93,24 +82,6 @@ namespace Regorus
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_execution_timer_config((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public void SetPolicyLengthConfig(PolicyLengthConfig config)
{
var nativeConfig = config.ToNative();
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_set_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr, nativeConfig));
});
}
public void ClearPolicyLengthConfig()
{
UseHandle(enginePtr =>
{
CheckAndDropResult(Regorus.Internal.API.regorus_engine_clear_policy_length_config((Regorus.Internal.RegorusEngine*)enginePtr));
});
}
public string? AddPolicy(string path, string rego)
{
return Utf8Marshaller.WithUtf8(path, pathPtr =>

View File

@@ -428,18 +428,6 @@ namespace Regorus.Internal
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_execution_timer_config(RegorusEngine* engine);
/// <summary>
/// Set the policy length limits for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_set_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_set_policy_length_config(RegorusEngine* engine, RegorusPolicyLengthConfig config);
/// <summary>
/// Clear the policy length configuration for a specific engine instance.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_clear_policy_length_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_clear_policy_length_config(RegorusEngine* engine);
#endregion
#region Execution Timer Global Methods
@@ -458,22 +446,6 @@ namespace Regorus.Internal
#endregion
#region Cache Configuration Global Methods
/// <summary>
/// Configure the global pattern caches used by regex and glob builtins.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_set_cache_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_set_cache_config(RegorusCacheConfig config);
/// <summary>
/// Clear all entries from every pattern cache.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_clear_cache", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_clear_cache();
#endregion
#region Compilation Methods
/// <summary>
@@ -520,16 +492,6 @@ namespace Regorus.Internal
#endregion
#region RBAC Methods
/// <summary>
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_rbac_engine_eval_condition", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_rbac_engine_eval_condition(byte* condition, byte* context_json);
#endregion
#region Target Registry Methods
/// <summary>
@@ -669,55 +631,6 @@ namespace Regorus.Internal
internal static extern RegorusResult regorus_effect_schema_clear();
#endregion
#region Alias Registry Methods
/// <summary>
/// Create a new, empty AliasRegistry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusAliasRegistry* regorus_alias_registry_new();
/// <summary>
/// Drop an AliasRegistry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry);
/// <summary>
/// Load control-plane alias data (array of ProviderAliases) into the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json);
/// <summary>
/// Load a data-plane policy manifest into the registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json);
/// <summary>
/// Return the number of resource types loaded in the alias registry.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_len(RegorusAliasRegistry* registry);
/// <summary>
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
/// Returns a JSON string.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_normalize_and_wrap", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_normalize_and_wrap(
RegorusAliasRegistry* registry, byte* resource_json, byte* api_version, byte* context_json, byte* parameters_json);
/// <summary>
/// Denormalize a previously-normalized resource JSON back to ARM format.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_denormalize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_alias_registry_denormalize(
RegorusAliasRegistry* registry, byte* normalized_json, byte* api_version);
#endregion
}
#region Native Structures
@@ -849,27 +762,6 @@ namespace Regorus.Internal
public uint check_interval;
}
/// <summary>
/// FFI representation of the policy length configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusPolicyLengthConfig
{
public uint max_col;
public UIntPtr max_file_bytes;
public UIntPtr max_lines;
}
/// <summary>
/// FFI representation of the cache configuration.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RegorusCacheConfig
{
public UIntPtr regex;
public UIntPtr glob;
}
/// <summary>
/// Byte buffer returned from FFI.
/// </summary>
@@ -923,13 +815,5 @@ namespace Regorus.Internal
public byte* content;
}
/// <summary>
/// Wrapper for AliasRegistry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe partial struct RegorusAliasRegistry
{
}
#endregion
}

View File

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

View File

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

View File

@@ -183,52 +183,4 @@ namespace Regorus
return true;
}
}
internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private RegorusAliasRegistryHandle() : base(ownsHandle: true)
{
}
internal static RegorusAliasRegistryHandle Create()
{
unsafe
{
var raw = Internal.API.regorus_alias_registry_new();
if (raw is null)
{
throw new InvalidOperationException("Failed to create Regorus alias registry.");
}
var handle = new RegorusAliasRegistryHandle();
handle.SetHandle((IntPtr)raw);
return handle;
}
}
internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
}
var handle = new RegorusAliasRegistryHandle();
handle.SetHandle(pointer);
return handle;
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
unsafe
{
Internal.API.regorus_alias_registry_drop((Internal.RegorusAliasRegistry*)handle);
}
SetHandle(IntPtr.Zero);
}
return true;
}
}
}

View File

@@ -18,13 +18,8 @@ var w = new Stopwatch();
w.Restart();
// Configure the global pattern caches.
Regorus.Engine.SetCacheConfig(new Regorus.CacheConfig(regex: 256, glob: 128));
var engine = new Regorus.Engine();
engine.SetRegoV0(true);
// Raise the default col limit to 2000
engine.SetPolicyLengthConfig(new Regorus.PolicyLengthConfig(maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000));
w.Stop();
var newEngineTicks = w.ElapsedTicks;

562
bindings/ffi/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,8 @@ default = [
"coverage",
"allocator-memory-limits",
"rvm",
"rbac",
"regorus/arc",
"regorus/full-opa",
"cache",
"contention_checks",
]
ast = ["regorus/ast"]
@@ -45,8 +43,6 @@ coverage = ["regorus/coverage"]
allocator-memory-limits = ["regorus/allocator-memory-limits"]
contention_checks = ["parking_lot"]
rvm = ["regorus/rvm"]
rbac = ["regorus/azure-rbac"]
cache = ["regorus/cache"]
custom_allocator = []
[build-dependencies]

View File

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

View File

@@ -11,7 +11,6 @@ use core::ffi::{c_char, c_longlong, c_void, CStr};
use core::{mem, ptr};
/// Status of a call on `RegorusEngine`.
#[derive(Debug, PartialEq)]
#[repr(C)]
pub enum RegorusStatus {
/// The operation was successful.

View File

@@ -6,7 +6,6 @@ use crate::common::{
};
use crate::compiled_policy::RegorusCompiledPolicy;
use crate::limits::RegorusExecutionTimerConfig;
use crate::limits::RegorusPolicyLengthConfig;
use crate::lock::{new_handle, read, try_read, try_write, Handle, ReadGuard, WriteGuard};
use crate::panic_guard::with_unwind_guard;
use alloc::boxed::Box;
@@ -492,37 +491,6 @@ pub extern "C" fn regorus_engine_clear_execution_timer_config(
}())
}
/// Set the policy length limits used when loading policies.
#[no_mangle]
pub extern "C" fn regorus_engine_set_policy_length_config(
engine: *mut RegorusEngine,
config: RegorusPolicyLengthConfig,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.set_policy_length_config(config.to_policy_length_config()?);
Ok(())
}())
})
}
/// Clear the policy length configuration, reverting to defaults.
#[no_mangle]
pub extern "C" fn regorus_engine_clear_policy_length_config(
engine: *mut RegorusEngine,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_result(|| -> Result<()> {
let engine = to_ref(engine)?;
let mut guard = engine.try_write()?;
guard.clear_policy_length_config();
Ok(())
}())
})
}
/// Get pretty printed coverage report.
///
/// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty

View File

@@ -5,7 +5,6 @@
extern crate alloc;
mod alias_registry;
mod allocator;
mod common;
mod compile;
@@ -15,8 +14,6 @@ mod engine;
mod limits;
mod lock;
mod panic_guard;
#[cfg(feature = "rbac")]
mod rbac;
#[cfg(feature = "rvm")]
pub(crate) mod rvm;
mod schema_registry;

View File

@@ -4,7 +4,7 @@
use crate::common::{to_regorus_result, RegorusResult, RegorusStatus};
use alloc::format;
use anyhow::{anyhow, Result};
use core::num::{NonZeroU32, NonZeroUsize};
use core::num::NonZeroU32;
use core::time::Duration;
use regorus::utils::limits::{self, ExecutionTimerConfig};
@@ -158,31 +158,6 @@ impl RegorusExecutionTimerConfig {
}
}
/// FFI representation of [`regorus::PolicyLengthConfig`].
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RegorusPolicyLengthConfig {
/// Maximum column width per line (must be non-zero).
pub max_col: u32,
/// Maximum policy file size in bytes (must be non-zero).
pub max_file_bytes: usize,
/// Maximum number of lines per policy file (must be non-zero).
pub max_lines: usize,
}
impl RegorusPolicyLengthConfig {
pub fn to_policy_length_config(self) -> Result<regorus::PolicyLengthConfig> {
Ok(regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(self.max_col)
.ok_or_else(|| anyhow!("max_col must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(self.max_file_bytes)
.ok_or_else(|| anyhow!("max_file_bytes must be non-zero"))?,
max_lines: NonZeroUsize::new(self.max_lines)
.ok_or_else(|| anyhow!("max_lines must be non-zero"))?,
})
}
}
#[no_mangle]
pub extern "C" fn regorus_set_fallback_execution_timer_config(
config: RegorusExecutionTimerConfig,
@@ -199,40 +174,6 @@ pub extern "C" fn regorus_clear_fallback_execution_timer_config() -> RegorusResu
RegorusResult::ok_void()
}
// ---------------------------------------------------------------------------
// Cache configuration (global)
// ---------------------------------------------------------------------------
/// FFI representation of [`regorus::cache::Config`].
#[cfg(feature = "cache")]
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RegorusCacheConfig {
/// Maximum compiled regex patterns (default 256, 0 = disabled).
pub regex: usize,
/// Maximum compiled glob matchers (default 128, 0 = disabled).
pub glob: usize,
}
/// Configure the global pattern caches used by `regex.*` and `glob.*` builtins.
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "C" fn regorus_set_cache_config(config: RegorusCacheConfig) -> RegorusResult {
regorus::cache::configure(regorus::cache::Config {
regex: config.regex,
glob: config.glob,
});
RegorusResult::ok_void()
}
/// Clear all entries from every pattern cache.
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "C" fn regorus_clear_cache() -> RegorusResult {
regorus::cache::clear();
RegorusResult::ok_void()
}
#[cfg(test)]
mod tests {
use super::{

View File

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

View File

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

View File

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

634
bindings/java/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,12 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[features]
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
coverage = ["regorus/coverage"]
ast = ["regorus/ast"]
cache = ["regorus/cache"]
[dependencies]
anyhow = "1.0"
serde_json = "1.0.112"
jni = "0.22.4"
jni = "0.21.1"
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }

View File

@@ -1,9 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import com.microsoft.regorus.CacheConfig;
import com.microsoft.regorus.Engine;
import com.microsoft.regorus.PolicyLengthConfig;
import com.microsoft.regorus.PolicyModule;
import com.microsoft.regorus.Program;
import com.microsoft.regorus.Rvm;
@@ -11,9 +9,6 @@ import com.microsoft.regorus.Rvm;
public class Test {
public static void main(String[] args) {
// Configure the global pattern caches.
CacheConfig.configure(new CacheConfig(256, 128));
try (Engine engine = new Engine()) {
String pkg = engine.addPolicy(
"hello.rego",
@@ -31,9 +26,6 @@ public class Test {
// Enable coverage.
engine.setEnableCoverage(true);
// Raise the default col limit to 2000
engine.setPolicyLengthConfig(new PolicyLengthConfig(2000, 1048576, 20000));
// Evaluate rule.
String valueJson = engine.evalRule("data.test.message");
System.out.println(valueJson);

View File

@@ -97,7 +97,7 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
<version>3.5.4</version>
<configuration>
<!-- Add debug build to Java path, so it's discoverable by JVM. This is only for tests. -->
<argLine>-Djava.library.path=${project.basedir}/target/debug:${java.library.path}</argLine>

View File

@@ -2,11 +2,9 @@
// Licensed under the MIT License.
use anyhow::Result;
use core::num::{NonZeroU32, NonZeroUsize};
use jni::objects::{JBooleanArray, JByteArray, JClass, JObject, JObjectArray, JString};
use jni::strings::JNIString;
use jni::sys::{jboolean, jbooleanArray, jbyteArray, jlong, jobjectArray, jstring};
use jni::{jni_str, Env, EnvUnowned, Outcome};
use jni::JNIEnv;
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
@@ -18,7 +16,7 @@ use std::sync::Arc;
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
) -> jlong {
let engine = Engine::new();
@@ -27,7 +25,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jlong {
@@ -38,7 +36,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
enable: bool,
@@ -52,7 +50,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
@@ -60,9 +58,9 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = path.try_to_string(env)?;
let rego: String = rego.try_to_string(env)?;
let pkg = JString::new(env, engine.add_policy(path, rego)?)?;
let path: String = env.get_string(&path)?.into();
let rego: String = env.get_string(&rego)?.into();
let pkg = env.new_string(engine.add_policy(path, rego)?)?;
Ok(pkg.into_raw())
});
@@ -74,15 +72,15 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = path.try_to_string(env)?;
let pkg = JString::new(env, engine.add_policy_from_file(path)?)?;
let path: String = env.get_string(&path)?.into();
let pkg = env.new_string(engine.add_policy_from_file(path)?)?;
Ok(pkg.into_raw())
});
@@ -94,14 +92,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPackages(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let packages = engine.get_packages()?;
let packages_json = JString::new(env, serde_json::to_string_pretty(&packages)?)?;
let packages_json = env.new_string(serde_json::to_string_pretty(&packages)?)?;
Ok(packages_json.into_raw())
});
@@ -113,14 +111,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPackages(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPolicies(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let policies = engine.get_policies_as_json()?;
let policies_json = JString::new(env, &policies)?;
let policies_json = env.new_string(&policies)?;
Ok(policies_json.into_raw())
});
@@ -132,7 +130,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPolicies(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearData(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) {
@@ -145,14 +143,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearData(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJson(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
data: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let data: String = data.try_to_string(env)?;
let data: String = env.get_string(&data)?.into();
engine.add_data_json(&data)?;
Ok(())
});
@@ -160,14 +158,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJson(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFile(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = path.try_to_string(env)?;
let path: String = env.get_string(&path)?.into();
engine.add_data(Value::from_json_file(path)?)?;
Ok(())
});
@@ -175,14 +173,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFi
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJson(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
input: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let input: String = input.try_to_string(env)?;
let input: String = env.get_string(&input)?.into();
engine.set_input_json(&input)?;
Ok(())
});
@@ -190,14 +188,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJson(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromFile(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = path.try_to_string(env)?;
let path: String = env.get_string(&path)?.into();
engine.set_input(Value::from_json_file(path)?);
Ok(())
});
@@ -205,16 +203,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromF
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalQuery(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
query: JString,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let query: String = query.try_to_string(env)?;
let query: String = env.get_string(&query)?.into();
let results = engine.eval_query(query, false)?;
let output = JString::new(env, serde_json::to_string(&results)?)?;
let output = env.new_string(serde_json::to_string(&results)?)?;
Ok(output.into_raw())
});
@@ -226,16 +224,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalQuery(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalRule(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
rule: JString,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let rule: String = rule.try_to_string(env)?;
let rule: String = env.get_string(&rule)?.into();
let value = engine.eval_rule(rule)?;
let output = JString::new(env, value.to_json_str()?)?;
let output = env.new_string(value.to_json_str()?)?;
Ok(output.into_raw())
});
@@ -248,7 +246,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalRule(
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetEnableCoverage(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
enable: bool,
@@ -263,14 +261,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetEnableCoverage
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetCoverageReport(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let report = engine.get_coverage_report()?;
let output = JString::new(env, serde_json::to_string_pretty(&report)?)?;
let output = env.new_string(serde_json::to_string_pretty(&report)?)?;
Ok(output.into_raw())
});
@@ -283,14 +281,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetCoverageReport
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetCoverageReportPretty(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let report = engine.get_coverage_report()?.to_string_pretty()?;
let output = JString::new(env, &report)?;
let output = env.new_string(&report)?;
Ok(output.into_raw())
});
@@ -303,7 +301,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetCoverageReport
#[no_mangle]
#[cfg(feature = "coverage")]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearCoverageData(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) {
@@ -316,7 +314,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearCoverageData
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetGatherPrints(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
b: bool,
@@ -330,14 +328,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetGatherPrints(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeTakePrints(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let prints = engine.take_prints()?;
let output = JString::new(env, serde_json::to_string_pretty(&prints)?)?;
let output = env.new_string(serde_json::to_string_pretty(&prints)?)?;
Ok(output.into_raw())
});
@@ -350,14 +348,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeTakePrints(
#[no_mangle]
#[cfg(feature = "ast")]
pub extern "system" fn Java_com_microsoft_regorus_Engine_getAstAsJson(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let ast = engine.get_ast_as_json()?;
let output = JString::new(env, &ast)?;
let output = env.new_string(&ast)?;
Ok(output.into_raw())
});
@@ -367,73 +365,9 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_getAstAsJson(
}
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetPolicyLengthConfig(
env: EnvUnowned,
_class: JClass,
engine_ptr: jlong,
max_col: u32,
max_file_bytes: jlong,
max_lines: jlong,
) {
let _ = throw_err(env, |_env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
engine.set_policy_length_config(regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(max_col)
.ok_or_else(|| anyhow::anyhow!("maxCol must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(max_file_bytes as usize)
.ok_or_else(|| anyhow::anyhow!("maxFileBytes must be non-zero"))?,
max_lines: NonZeroUsize::new(max_lines as usize)
.ok_or_else(|| anyhow::anyhow!("maxLines must be non-zero"))?,
});
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearPolicyLengthConfig(
_env: EnvUnowned,
_class: JClass,
engine_ptr: jlong,
) {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
engine.clear_policy_length_config();
}
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_CacheConfig_nativeSetCacheConfig(
_env: EnvUnowned,
_class: JClass,
regex: jlong,
glob: jlong,
) {
regorus::cache::configure(regorus::cache::Config {
regex: if regex < 0 {
0
} else {
usize::try_from(regex).unwrap_or(usize::MAX)
},
glob: if glob < 0 {
0
} else {
usize::try_from(glob).unwrap_or(usize::MAX)
},
});
}
#[cfg(feature = "cache")]
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_CacheConfig_nativeClearCache(
_env: EnvUnowned,
_class: JClass,
) {
regorus::cache::clear();
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) {
@@ -444,7 +378,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModules(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
data_json: JString,
module_ids: jobjectArray,
@@ -452,7 +386,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModul
entry_points: jobjectArray,
) -> jlong {
let res = throw_err(env, |env| {
let data_json: String = data_json.try_to_string(env)?;
let data_json: String = env.get_string(&data_json)?.into();
let data = Value::from_json_str(&data_json)?;
let ids = get_string_array(env, module_ids)?;
@@ -488,7 +422,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModul
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngine(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
entry_points: jobjectArray,
@@ -513,7 +447,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngin
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) -> jstring {
@@ -521,7 +455,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
let program = unsafe { &*(program_ptr as *mut Arc<RvmProgram>) };
let listing =
generate_assembly_listing(program.as_ref(), &AssemblyListingConfig::default());
let output = JString::new(env, &listing)?;
let output = env.new_string(&listing)?;
Ok(output.into_raw())
});
@@ -533,7 +467,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeSerializeBinary(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) -> jbyteArray {
@@ -557,7 +491,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeSerializeBinary(
/// for the duration of the call. They must come from the JVM for the current
/// thread and not be used after this function returns.
pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeserializeBinary(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
data: jbyteArray,
is_partial: jbooleanArray,
@@ -567,7 +501,7 @@ pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeseriali
return Err(anyhow::anyhow!("data must not be null"));
}
let data = unsafe { JByteArray::from_raw(env, data) };
let data = unsafe { JByteArray::from_raw(data) };
let bytes = env.convert_byte_array(&data)?;
let (program, partial) =
match RvmProgram::deserialize_binary(&bytes).map_err(|e| anyhow::anyhow!(e))? {
@@ -576,15 +510,11 @@ pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeseriali
};
if !is_partial.is_null() {
let is_partial = unsafe { JBooleanArray::from_raw(env, is_partial) };
let len = is_partial.len(env)?;
let is_partial = unsafe { JBooleanArray::from_raw(is_partial) };
let len = env.get_array_length(&is_partial)?;
if len > 0 {
let value: [jboolean; 1] = [if partial {
jni::sys::JNI_TRUE
} else {
jni::sys::JNI_FALSE
}];
is_partial.set_region(env, 0, &value)?;
let value: [jboolean; 1] = [if partial { 1 } else { 0 }];
env.set_boolean_array_region(&is_partial, 0, &value)?;
}
}
@@ -596,7 +526,7 @@ pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeseriali
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeDrop(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
program_ptr: jlong,
) {
@@ -607,7 +537,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeDrop(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
) -> jlong {
let vm = RegoVM::new();
@@ -616,7 +546,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
program_ptr: jlong,
@@ -631,14 +561,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
data_json: JString,
) {
let _ = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let data_json: String = data_json.try_to_string(env)?;
let data_json: String = env.get_string(&data_json)?.into();
let data = Value::from_json_str(&data_json)?;
vm.set_data(data)?;
Ok(())
@@ -647,14 +577,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
input_json: JString,
) {
let _ = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let input_json: String = input_json.try_to_string(env)?;
let input_json: String = env.get_string(&input_json)?.into();
let input = Value::from_json_str(&input_json)?;
vm.set_input(input);
Ok(())
@@ -663,7 +593,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
mode: u8,
@@ -682,14 +612,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let result = vm.execute()?;
let output = JString::new(env, result.to_json_str()?)?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
@@ -701,16 +631,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
entry_point: JString,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let entry_point: String = entry_point.try_to_string(env)?;
let entry_point: String = env.get_string(&entry_point)?.into();
let result = vm.execute_entry_point_by_name(&entry_point)?;
let output = JString::new(env, result.to_json_str()?)?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
@@ -722,7 +652,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
resume_json: JString,
@@ -731,13 +661,13 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let value = if has_value {
let resume_json: String = resume_json.try_to_string(env)?;
let resume_json: String = env.get_string(&resume_json)?.into();
Some(Value::from_json_str(&resume_json)?)
} else {
None
};
let result = vm.resume(value)?;
let output = JString::new(env, result.to_json_str()?)?;
let output = env.new_string(result.to_json_str()?)?;
Ok(output.into_raw())
});
@@ -749,13 +679,13 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState(
env: EnvUnowned,
env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) -> jstring {
let res = throw_err(env, |env| {
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
let output = JString::new(env, format!("{:?}", vm.execution_state()))?;
let output = env.new_string(format!("{:?}", vm.execution_state()))?;
Ok(output.into_raw())
});
@@ -767,7 +697,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState(
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop(
_env: EnvUnowned,
_env: JNIEnv,
_class: JClass,
vm_ptr: jlong,
) {
@@ -776,57 +706,27 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop(
}
}
fn throw_err<T>(mut env: EnvUnowned, f: impl FnOnce(&mut Env) -> Result<T>) -> Result<T> {
let outcome = env.with_env(|env| -> Result<T> {
match f(env) {
Ok(val) => Ok(val),
Err(err) => {
if let Err(throw_err) = env.throw_new(
jni_str!("java/lang/RuntimeException"),
JNIString::new(err.to_string()),
) {
return Err(anyhow::anyhow!(
"Failed to throw Java RuntimeException for error '{err}': {throw_err}"
));
}
Err(err)
}
}
});
match outcome.into_outcome() {
Outcome::Ok(val) => Ok(val),
Outcome::Err(err) => Err(err),
Outcome::Panic(payload) => {
let msg = payload
.downcast_ref::<String>()
.map(|s| s.as_str())
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("unknown panic");
let err = anyhow::anyhow!("panic: {msg}");
// Try to surface the panic as a Java exception.
let _ = env.with_env(|env| -> Result<()> {
env.throw_new(
jni_str!("java/lang/RuntimeException"),
JNIString::new(format!("Rust panic: {msg}")),
)?;
Ok(())
});
fn throw_err<T>(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result<T>) -> Result<T> {
match f(&mut env) {
Ok(val) => Ok(val),
Err(err) => {
env.throw(err.to_string())?;
Err(err)
}
}
}
fn get_string_array(env: &mut Env, array: jobjectArray) -> Result<Vec<String>> {
fn get_string_array(env: &mut JNIEnv, array: jobjectArray) -> Result<Vec<String>> {
if array.is_null() {
return Ok(Vec::new());
}
let array = unsafe { JObjectArray::<JObject>::from_raw(env, array) };
let len = array.len(env)?;
let mut values = Vec::with_capacity(len);
let array = unsafe { JObjectArray::from_raw(array) };
let len = env.get_array_length(&array)?;
let mut values = Vec::with_capacity(len as usize);
for i in 0..len {
let obj = array.get_element(env, i)?;
let jstr = unsafe { JString::from_raw(env, obj.into_raw()) };
let value: String = jstr.try_to_string(env)?;
let obj = env.get_object_array_element(&array, i)?;
let jstr = JString::from(obj);
let value: String = env.get_string(&jstr)?.into();
values.push(value);
}
Ok(values)

View File

@@ -1,62 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
/**
* Global configuration for compiled pattern caches used by regex and glob builtins.
*
* <p>Capacity of 0 disables the corresponding cache.
*/
public final class CacheConfig {
static {
System.loadLibrary("regorus_java");
}
private static native void nativeSetCacheConfig(long regex, long glob);
private static native void nativeClearCache();
/**
* Maximum cached compiled regex patterns (default 256).
*/
public final long regex;
/**
* Maximum cached compiled glob matchers (default 128).
*/
public final long glob;
/**
* Create a new cache configuration.
*
* @param regex Maximum cached compiled regex patterns (0 = disabled).
* @param glob Maximum cached compiled glob matchers (0 = disabled).
*/
public CacheConfig(long regex, long glob) {
if (regex < 0) {
throw new IllegalArgumentException("regex must be non-negative");
}
if (glob < 0) {
throw new IllegalArgumentException("glob must be non-negative");
}
this.regex = regex;
this.glob = glob;
}
/**
* Apply this cache configuration globally.
*/
public static void configure(CacheConfig config) {
nativeSetCacheConfig(config.regex, config.glob);
}
/**
* Clear all entries from every pattern cache.
*/
public static void clear() {
nativeClearCache();
}
}

View File

@@ -39,8 +39,6 @@ public class Engine implements AutoCloseable, Cloneable {
private static native void nativeClearCoverageData(long enginePtr);
private static native void nativeSetGatherPrints(long enginePtr, boolean b);
private static native String nativeTakePrints(long enginePtr);
private static native void nativeSetPolicyLengthConfig(long enginePtr, int maxCol, long maxFileBytes, long maxLines);
private static native void nativeClearPolicyLengthConfig(long enginePtr);
private static native void nativeDestroyEngine(long enginePtr);
// Pointer to Engine allocated on Rust's heap, all native methods works on
@@ -261,22 +259,6 @@ public class Engine implements AutoCloseable, Cloneable {
return nativeTakePrints(enginePtr);
}
/**
* Set the policy length limits used when loading policies.
*
* @param config Policy length configuration.
*/
public void setPolicyLengthConfig(PolicyLengthConfig config) {
nativeSetPolicyLengthConfig(enginePtr, config.maxCol, config.maxFileBytes, config.maxLines);
}
/**
* Clear the policy length configuration, reverting to defaults.
*/
public void clearPolicyLengthConfig() {
nativeClearPolicyLengthConfig(enginePtr);
}
long getPtr() {
return enginePtr;
}

View File

@@ -1,50 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
/**
* Policy source length limits enforced when loading policy files.
*
* All values must be positive (non-zero).
*/
public final class PolicyLengthConfig {
/**
* Maximum column width per line (default: 1024).
*/
public final int maxCol;
/**
* Maximum policy file size in bytes (default: 1 MiB).
*/
public final long maxFileBytes;
/**
* Maximum number of lines per policy file (default: 20000).
*/
public final long maxLines;
/**
* Create a new policy length configuration.
*
* @param maxCol Maximum column width per line.
* @param maxFileBytes Maximum policy file size in bytes.
* @param maxLines Maximum number of lines per policy file.
*/
public PolicyLengthConfig(int maxCol, long maxFileBytes, long maxLines) {
if (maxCol <= 0) {
throw new IllegalArgumentException("maxCol must be positive");
}
if (maxFileBytes <= 0) {
throw new IllegalArgumentException("maxFileBytes must be positive");
}
if (maxLines <= 0) {
throw new IllegalArgumentException("maxLines must be positive");
}
this.maxCol = maxCol;
this.maxFileBytes = maxFileBytes;
this.maxLines = maxLines;
}
}

View File

@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"getrandom",
"once_cell",
"serde",
"version_check",
@@ -25,12 +25,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -42,9 +36,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
@@ -52,6 +46,22 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -69,9 +79,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "borrow-or-share"
@@ -91,9 +101,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.20.2"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecount"
@@ -103,9 +113,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]]
name = "cc"
version = "1.2.58"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -117,22 +127,11 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -151,30 +150,12 @@ dependencies = [
"phf",
]
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
@@ -201,18 +182,6 @@ dependencies = [
"serde",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -221,9 +190,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fancy-regex"
version = "0.17.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
@@ -238,27 +207,15 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
version = "0.4.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -285,25 +242,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasip2",
"wasip3",
]
[[package]]
@@ -318,25 +259,11 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
@@ -449,12 +376,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -483,28 +404,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
name = "ipnet"
version = "2.12.0"
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "itoa"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.91"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -512,28 +442,27 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
dependencies = [
"ahash",
"base64",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -543,17 +472,11 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.183"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "litemap"
@@ -577,16 +500,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.3"
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "memchr"
version = "2.8.0"
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "msvc_spectre_libs"
@@ -678,15 +604,15 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.4"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "ordered-float"
version = "5.3.0"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d"
dependencies = [
"num-traits",
]
@@ -746,21 +672,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.13.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "postcard"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"serde",
]
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
[[package]]
name = "potential_utf"
@@ -772,13 +686,12 @@ dependencies = [
]
[[package]]
name = "prettyplease"
version = "0.2.37"
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"proc-macro2",
"syn",
"zerocopy",
]
[[package]]
@@ -792,33 +705,38 @@ dependencies = [
[[package]]
name = "pyo3"
version = "0.28.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1"
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
dependencies = [
"anyhow",
"cfg-if",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.28.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7"
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.28.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc"
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
dependencies = [
"libc",
"pyo3-build-config",
@@ -826,9 +744,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.28.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e"
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -838,9 +756,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.28.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a"
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
dependencies = [
"heck",
"proc-macro2",
@@ -851,9 +769,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -865,27 +783,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand"
version = "0.10.0"
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.0"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
@@ -918,14 +842,13 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -933,9 +856,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.3"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -945,9 +868,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.14"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -956,15 +879,16 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.10"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "regorus"
version = "0.9.1"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"data-encoding",
@@ -973,12 +897,9 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
"lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
"parking_lot",
"postcard",
"rand",
"regex",
"regorus-mimalloc",
@@ -1025,9 +946,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "scopeguard"
@@ -1117,9 +1038,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spin"
version = "0.10.0"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
@@ -1129,9 +1050,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "syn"
version = "2.0.117"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -1151,9 +1072,9 @@ dependencies = [
[[package]]
name = "target-lexicon"
version = "0.13.5"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba"
[[package]]
name = "thiserror"
@@ -1185,23 +1106,17 @@ dependencies = [
"zerovec",
]
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.24"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-xid"
version = "0.2.6"
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "unsafe-libyaml"
@@ -1209,6 +1124,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1229,12 +1150,14 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [
"getrandom 0.4.2",
"getrandom",
"js-sys",
"rand",
"wasm-bindgen",
]
[[package]]
@@ -1244,6 +1167,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -1268,20 +1192,11 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -1292,9 +1207,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1302,9 +1217,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1315,47 +1230,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -1420,88 +1301,6 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"
@@ -1534,18 +1333,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.47"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.47"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",
@@ -1608,6 +1407,6 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.21"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"

View File

@@ -15,15 +15,14 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
crate-type = ["cdylib"]
[features]
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
ast = ["regorus/ast"]
cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
anyhow = "1.0"
ordered-float = "5.3.0"
pyo3 = { version = "0.28.2", features = ["abi3-py310", "anyhow", "extension-module"] }
ordered-float = "5.0.0"
pyo3 = { version = "0.24.1", features = ["abi3-py310", "anyhow", "extension-module"] }
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
serde_json = "1.0.140"

View File

@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, Result};
use core::num::{NonZeroU32, NonZeroUsize};
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::*;
@@ -44,7 +43,7 @@ impl Default for Engine {
fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
// dicts
Ok(if let Ok(dict) = ob.cast::<PyDict>() {
Ok(if let Ok(dict) = ob.downcast::<PyDict>() {
let mut map = BTreeMap::new();
for (k, v) in dict {
map.insert(from(&k)?, from(&v)?);
@@ -52,7 +51,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
map.into()
}
// set
else if let Ok(pset) = ob.cast::<PySet>() {
else if let Ok(pset) = ob.downcast::<PySet>() {
let mut set = BTreeSet::new();
for v in pset {
set.insert(from(&v)?);
@@ -60,7 +59,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
set.into()
}
// frozen set
else if let Ok(pfset) = ob.cast::<PyFrozenSet>() {
else if let Ok(pfset) = ob.downcast::<PyFrozenSet>() {
//
let mut set = BTreeSet::new();
for v in pfset {
@@ -69,13 +68,13 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
set.into()
}
// lists and tuples
else if let Ok(plist) = ob.cast::<PyList>() {
else if let Ok(plist) = ob.downcast::<PyList>() {
let mut array = Vec::new();
for v in plist {
array.push(from(&v)?);
}
array.into()
} else if let Ok(ptuple) = ob.cast::<PyTuple>() {
} else if let Ok(ptuple) = ob.downcast::<PyTuple>() {
let mut array = Vec::new();
for v in ptuple {
array.push(from(&v)?);
@@ -86,10 +85,6 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
else if let Ok(s) = ob.extract::<String>() {
s.into()
}
// Boolean
else if let Ok(b) = ob.extract::<bool>() {
b.into()
}
// Numeric
else if let Ok(v) = ob.extract::<i64>() {
v.into()
@@ -98,12 +93,16 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
} else if let Ok(v) = ob.extract::<f64>() {
v.into()
}
// Boolean
else if let Ok(b) = ob.extract::<bool>() {
b.into()
}
// None
else if ob.cast::<PyNone>().is_ok() {
else if ob.downcast::<PyNone>().is_ok() {
Value::Null
}
// Anything that is a sequence
else if let Ok(pseq) = ob.cast::<PySequence>() {
else if let Ok(pseq) = ob.downcast::<PySequence>() {
let mut array = Vec::new();
for i in 0..pseq.len()? {
array.push(from(&pseq.get_item(i)?)?);
@@ -111,7 +110,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
array.into()
}
// Anything that is a map
else if let Ok(pmap) = ob.cast::<PyMapping>() {
else if let Ok(pmap) = ob.downcast::<PyMapping>() {
let mut map = BTreeMap::new();
let keys = pmap.keys()?;
let values = pmap.values()?;
@@ -128,7 +127,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
})
}
fn to(mut v: Value, py: Python<'_>) -> Result<Py<PyAny>> {
fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
let obj = match v {
Value::Null => None::<u64>.into_bound_py_any(py),
@@ -139,17 +138,12 @@ fn to(mut v: Value, py: Python<'_>) -> Result<Py<PyAny>> {
Value::String(s) => s.into_bound_py_any(py),
Value::Number(_) => {
if v.as_number()?.is_integer() {
if let Ok(u) = v.as_u64() {
u.into_bound_py_any(py)
} else {
v.as_i64()?.into_bound_py_any(py)
}
} else if let Ok(f) = v.as_f64() {
if let Ok(f) = v.as_f64() {
f.into_bound_py_any(py)
} else if let Ok(u) = v.as_u64() {
u.into_bound_py_any(py)
} else {
// fallback
v.as_f64()?.into_bound_py_any(py)
v.as_i64()?.into_bound_py_any(py)
}
}
@@ -297,7 +291,7 @@ impl Engine {
/// Evaluate query.
///
/// * `query`: Rego expression to be evaluate.
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<Py<PyAny>> {
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<PyObject> {
let results = self.engine.eval_query(query, false)?;
let rlist = PyList::empty(py);
@@ -338,7 +332,7 @@ impl Engine {
/// Evaluate rule.
///
/// * `rule`: Full path to the rule.
pub fn eval_rule(&mut self, rule: String, py: Python<'_>) -> Result<Py<PyAny>> {
pub fn eval_rule(&mut self, rule: String, py: Python<'_>) -> Result<PyObject> {
to(self.engine.eval_rule(rule)?, py)
}
@@ -350,78 +344,6 @@ impl Engine {
v.to_json_str()
}
/// Registers a custom Python function as a Rego extension.
///
/// This allows you to define functions in Python that can be called directly
/// from your Rego policies. The Python function will be called synchronously
/// during policy evaluation.
///
/// Arguments passed from Rego are automatically converted to their corresponding
/// Python types. The return value is converted back to a Rego value.
///
/// * `path`: Full path to the function as it will be used in Rego.
/// * `nargs`: The number of arguments the function expects.
/// * `extension`: The Python function to execute. Must accept exactly `nargs` arguments.
///
/// Note: When the engine is cloned, extensions share the same Python callable reference
/// rather than being deep-copied. Stateful callables will share state across clones.
pub fn add_extension(&mut self, path: String, nargs: u8, extension: Py<PyAny>) -> Result<()> {
Python::attach(|py| {
if !extension.bind(py).is_callable() {
return Err(anyhow!("extension '{}' must be callable", path));
}
Ok(())
})?;
let func_ref = Arc::new(extension);
let path_clone = path.clone();
let extension_impl = move |args: Vec<Value>| -> Result<Value, anyhow::Error> {
Python::attach(|py| {
let py_args_vec: Result<Vec<Py<PyAny>>> =
args.into_iter().map(|arg| to(arg, py)).collect();
let py_args = PyTuple::new(py, py_args_vec?)?;
let py_result = func_ref.call1(py, py_args).map_err(|e| {
anyhow!("extension '{}' raises Python error: {}", path_clone, e)
})?;
let rego_result = from(&py_result.into_bound(py))?;
Ok(rego_result)
})
};
self.engine
.add_extension(path, nargs, Box::new(extension_impl))
}
/// Set the policy length limits used when loading policies.
///
/// * `max_col`: Maximum column width per line.
/// * `max_file_bytes`: Maximum policy file size in bytes.
/// * `max_lines`: Maximum number of lines per policy file.
#[pyo3(signature = (*, max_col, max_file_bytes, max_lines))]
pub fn set_policy_length_config(
&mut self,
max_col: u32,
max_file_bytes: usize,
max_lines: usize,
) -> Result<()> {
self.engine
.set_policy_length_config(::regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(max_col)
.ok_or_else(|| anyhow!("max_col must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(max_file_bytes)
.ok_or_else(|| anyhow!("max_file_bytes must be non-zero"))?,
max_lines: NonZeroUsize::new(max_lines)
.ok_or_else(|| anyhow!("max_lines must be non-zero"))?,
});
Ok(())
}
/// Clear the policy length configuration, reverting to defaults.
pub fn clear_policy_length_config(&mut self) {
self.engine.clear_policy_length_config();
}
/// Enable code coverage
///
/// * `enable`: Whether to enable coverage or not.
@@ -622,33 +544,10 @@ impl Rvm {
}
}
/// Configure the global pattern caches used by `regex.*` and `glob.*` builtins.
///
/// * `regex`: Maximum cached compiled regex patterns (default 256, 0 = disabled).
/// * `glob`: Maximum cached compiled glob matchers (default 128, 0 = disabled).
#[cfg(feature = "cache")]
#[pyfunction]
#[pyo3(signature = (*, regex = 256, glob = 128))]
fn set_cache_config(regex: usize, glob: usize) {
::regorus::cache::configure(::regorus::cache::Config { regex, glob });
}
/// Clear all entries from every pattern cache.
#[cfg(feature = "cache")]
#[pyfunction]
fn clear_cache() {
::regorus::cache::clear();
}
#[pymodule]
pub fn regorus(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<crate::Engine>()?;
m.add_class::<crate::Program>()?;
m.add_class::<crate::Rvm>()?;
#[cfg(feature = "cache")]
{
m.add_function(wrap_pyfunction!(set_cache_config, m)?)?;
m.add_function(wrap_pyfunction!(clear_cache, m)?)?;
}
Ok(())
}

View File

@@ -1,20 +1,16 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import regorus
import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
# Configure the global pattern caches.
regorus.set_cache_config(regex=256, glob=128)
# Create engine
engine = regorus.Engine()
engine.set_rego_v0(True)
# Raise the default col limit to 2000
engine.set_policy_length_config(max_col=2000, max_file_bytes=1048576, max_lines=20000)
# Load policies
pkg = engine.add_policy_from_file('../../tests/aci/framework.rego')
@@ -167,254 +163,3 @@ def run_host_await_example():
print(vm.resume('{"tier":"gold"}'))
run_host_await_example()
def test_extension_execution():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting(a, b) if {
a := data.a
b := data.b
}
""")
def custom_function(arg1, arg2):
return f"{arg1}, {arg2}!"
rego.add_extension("greeting", 2, custom_function)
rego.add_data({"a": "Hello", "b": "World"})
result = rego.eval_rule("data.demo.result")
assert result == "Hello, World!", f"Unexpected result: {result}"
test_extension_execution()
def test_extension_wrong_arity():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting(a, b) if {
a := data.a
b := data.b
}
""")
def custom_function(arg1, arg2):
return f"{arg1}, {arg2}!"
rego.add_extension("greeting", 3, custom_function)
rego.add_data({"a": "Hello", "b": "World"})
try:
rego.eval_rule("data.demo.result")
except RuntimeError as ex:
assert "error: incorrect number of parameters supplied to extension" in str(ex)
else:
assert False, "exception not thrown"
test_extension_wrong_arity()
def test_extension_raises_exception():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting(a, b) if {
a := data.a
b := data.b
}
""")
def custom_function(arg1, arg2):
raise RuntimeError("unknown error")
rego.add_extension("greeting", 2, custom_function)
rego.add_data({"a": "Hello", "b": "World"})
try:
rego.eval_rule("data.demo.result")
except RuntimeError as ex:
assert "error: extension 'greeting' raises Python error: RuntimeError: unknown error" in str(ex)
else:
assert False, "exception not thrown"
test_extension_raises_exception()
def test_extension_zero_arg():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting()
""")
def custom_function():
return "Hello, World!"
rego.add_extension("greeting", 0, custom_function)
rego.add_data({"a": "Hello", "b": "World"})
result = rego.eval_rule("data.demo.result")
assert result == "Hello, World!", f"Unexpected result: {result}"
test_extension_zero_arg()
def test_extension_non_callable():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting()
""")
try:
rego.add_extension("greeting", 0, 123)
except RuntimeError as ex:
assert "extension 'greeting' must be callable" in str(ex)
else:
assert False, "exception not thrown"
test_extension_non_callable()
def test_extension_duplicate():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result := greeting()
""")
def custom_function1(arg1, arg2):
return f"{arg1}, {arg2}!"
def custom_function2(arg1, arg2):
return f"{arg1}, {arg2}!"
rego.add_extension("greeting", 0, custom_function1)
try:
rego.add_extension("greeting", 0, custom_function2)
except RuntimeError as ex:
assert "extension already added" in str(ex)
else:
assert False, "exception not thrown"
test_extension_duplicate()
def test_extension_types():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
i := custom.triple(10)
f := custom.triple(2.5)
b1 := custom.negate(true)
b2 := custom.negate(false)
a := custom.first([true, null, 1])
b := custom.first([null, null, 1])
c := custom.first([null, null, null])
object := custom.modify_object({"a": 1, "b": 2})
list := custom.modify_list([3, 4])
set := custom.modify_set({5, 6})
""")
def triple(n):
return n*3
def negate(b):
return not b
def first(lst):
for i in lst:
if i is not None:
return i
return None
def modify_object(object):
assert isinstance(object, dict)
return {k: v*2 for k, v in object.items()}
def modify_list(lst):
assert isinstance(lst, list)
return [x*2 for x in lst]
def modify_set(st):
assert isinstance(st, set)
return {x*2 for x in st}
rego.add_extension("custom.triple", 1, triple)
rego.add_extension("custom.negate", 1, negate)
rego.add_extension("custom.first", 1, first)
rego.add_extension("custom.modify_object", 1, modify_object)
rego.add_extension("custom.modify_list", 1, modify_list)
rego.add_extension("custom.modify_set", 1, modify_set)
i = rego.eval_rule("data.demo.i")
assert i == 30, f"Unexpected result for 'i': {i}"
f = rego.eval_rule("data.demo.f")
assert f == 7.5, f"Unexpected result for 'f': {f}"
b1 = rego.eval_rule("data.demo.b1")
assert b1 == False, f"Unexpected result for 'b1': {b1}"
b2 = rego.eval_rule("data.demo.b2")
assert b2 == True, f"Unexpected result for 'b2': {b2}"
a = rego.eval_rule("data.demo.a")
assert a == True, f"Unexpected result for 'a': {a}"
b = rego.eval_rule("data.demo.b")
assert b == 1, f"Unexpected result for 'b': {b}"
c = rego.eval_rule("data.demo.c")
assert c is None, f"Unexpected result for 'c': {c}"
obj = rego.eval_rule("data.demo.object")
assert obj == {"a": 2, "b": 4}, f"Unexpected object: {obj}"
lst = rego.eval_rule("data.demo.list")
assert lst == [6, 8], f"Unexpected list: {lst}"
st = rego.eval_rule("data.demo.set")
assert st == {10, 12}, f"Unexpected set: {st}"
test_extension_types()
def test_boolean_mapping():
rego = regorus.Engine()
rego.add_policy("demo",
"""
package demo
result_b := data.a if {
data.a == true
}
result_i := data.b if {
data.b == 1
}
""")
rego.add_data({"a": True, "b": 1})
result_b = rego.eval_rule("data.demo.result_b")
assert isinstance(result_b, bool), f"Expected bool, got {type(result_b)}"
assert result_b, f"Unexpected result for 'result_b': {result_b}"
result_i = rego.eval_rule("data.demo.result_i")
assert isinstance(result_i, int), f"Expected int, got {type(result_i)}"
assert result_i == 1, f"Unexpected result for 'result_i': {result_i}"
test_boolean_mapping()

335
bindings/ruby/Cargo.lock generated
View File

@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"getrandom",
"once_cell",
"serde",
"version_check",
@@ -25,12 +25,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -42,9 +36,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
@@ -52,6 +46,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
version = "0.69.5"
@@ -146,22 +146,11 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -197,15 +186,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
@@ -246,9 +226,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fancy-regex"
version = "0.17.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
@@ -263,27 +243,15 @@ checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "fluent-uri"
version = "0.4.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -310,25 +278,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasip2",
"wasip3",
]
[[package]]
@@ -349,31 +301,11 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
@@ -480,12 +412,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -514,16 +440,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
"hashbrown",
]
[[package]]
name = "ipnet"
version = "2.12.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "itertools"
@@ -552,28 +476,27 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
dependencies = [
"ahash",
"base64",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -589,12 +512,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.180"
@@ -632,12 +549,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
[[package]]
name = "magnus"
version = "0.8.2"
@@ -840,13 +751,12 @@ dependencies = [
]
[[package]]
name = "prettyplease"
version = "0.2.37"
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"proc-macro2",
"syn",
"zerocopy",
]
[[package]]
@@ -874,27 +784,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand"
version = "0.10.0"
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.0"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
[[package]]
name = "rb-sys"
@@ -957,14 +873,13 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -972,9 +887,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.3"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -1008,15 +923,12 @@ dependencies = [
"chrono-tz",
"data-encoding",
"globset",
"indexmap",
"ipnet",
"jsonschema",
"lazy_static",
"lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
"parking_lot",
"rand",
"regex",
"regorus-mimalloc",
@@ -1050,7 +962,6 @@ version = "0.9.1"
dependencies = [
"magnus",
"regorus",
"serde",
"serde_json",
"serde_magnus",
]
@@ -1184,9 +1095,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spin"
version = "0.10.0"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
@@ -1252,24 +1163,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@@ -1296,12 +1195,14 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.4.2",
"getrandom",
"js-sys",
"rand",
"wasm-bindgen",
]
[[package]]
@@ -1311,6 +1212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -1335,15 +1237,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
@@ -1389,40 +1282,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -1487,88 +1346,6 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"

View File

@@ -11,6 +11,6 @@ gem "minitest", "~> 6.0"
gem "rake", "~> 13.3"
gem "rake-compiler", "~> 1.3"
gem "rake-compiler-dock", "~> 1.11"
gem "rubocop", "~> 1.86", require: false
gem "rubocop-minitest", "~> 0.39.1", require: false
gem "rubocop", "~> 1.84", require: false
gem "rubocop-minitest", "~> 0.38.2", require: false
gem "rubocop-rake", "~> 0.7.1", require: false

View File

@@ -8,15 +8,13 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.3)
drb (2.2.3)
json (2.19.2)
json (2.18.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (6.0.3)
drb (~> 2.0)
minitest (6.0.1)
prism (~> 1.5)
parallel (1.27.0)
parser (3.3.10.2)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
@@ -26,11 +24,10 @@ GEM
rake-compiler (1.3.1)
rake
rake-compiler-dock (1.11.0)
rb_sys (0.9.125)
json (>= 2)
rb_sys (0.9.124)
rake-compiler-dock (= 1.11.0)
regexp_parser (2.11.3)
rubocop (1.86.0)
rubocop (1.84.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -41,10 +38,10 @@ GEM
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-minitest (0.39.1)
rubocop-minitest (0.38.2)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
@@ -66,8 +63,8 @@ DEPENDENCIES
rake-compiler (~> 1.3)
rake-compiler-dock (~> 1.11)
regorusrb!
rubocop (~> 1.86)
rubocop-minitest (~> 0.39.1)
rubocop (~> 1.84)
rubocop-minitest (~> 0.38.2)
rubocop-rake (~> 0.7.1)
BUNDLED WITH

View File

@@ -11,14 +11,12 @@ crate-type = ["cdylib"]
path = "src/lib.rs"
[features]
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
ast = ["regorus/ast"]
cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
magnus = { version = "0.8.2" }
regorus = { path = "../../../..", default-features = false, features = ["arc"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.140"
serde_magnus = "0.11.0"

View File

@@ -1,26 +1,10 @@
use core::num::{NonZeroU32, NonZeroUsize};
use magnus::{Error, Ruby, exception::runtime_error, method, module, prelude::*};
use regorus::Engine as RegorusEngine;
use serde::Deserialize;
use std::cell::RefCell;
use std::cmp::Ordering;
// `Value` exists under magnus, regorus, and serde_json, so be explicit
#[derive(Deserialize)]
struct PolicyLengthSpec {
max_col: u32,
max_file_bytes: usize,
max_lines: usize,
}
#[cfg(feature = "cache")]
#[derive(Deserialize)]
struct CacheConfigSpec {
regex: usize,
glob: usize,
}
#[derive(Default)]
#[magnus::wrap(class = "Regorus::Engine")]
pub struct Engine {
@@ -71,17 +55,15 @@ impl Engine {
.map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {e}")))
}
fn add_data(ruby: &Ruby, rb_self: &Self, ruby_hash: magnus::RHash) -> Result<(), Error> {
let data_value: regorus::Value =
serde_magnus::deserialize(ruby, ruby_hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize Ruby value: {e}"),
)
})?;
fn add_data(&self, ruby_hash: magnus::RHash) -> Result<(), Error> {
let data_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize Ruby value: {e}"),
)
})?;
rb_self
.engine
self.engine
.borrow_mut()
.add_data(data_value)
.map_err(|e| Error::new(runtime_error(), format!("Failed to add data: {e}")))
@@ -129,16 +111,15 @@ impl Engine {
.map_err(|e| Error::new(runtime_error(), format!("Failed to get policies: {e}")))
}
fn set_input(ruby: &Ruby, rb_self: &Self, ruby_hash: magnus::RHash) -> Result<(), Error> {
let input_value: regorus::Value =
serde_magnus::deserialize(ruby, ruby_hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize Ruby value: {e}"),
)
})?;
fn set_input(&self, ruby_hash: magnus::RHash) -> Result<(), Error> {
let input_value: regorus::Value = serde_magnus::deserialize(ruby_hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize Ruby value: {e}"),
)
})?;
rb_self.engine.borrow_mut().set_input(input_value);
self.engine.borrow_mut().set_input(input_value);
Ok(())
}
@@ -161,14 +142,14 @@ impl Engine {
Ok(())
}
fn eval_query(ruby: &Ruby, rb_self: &Self, query: String) -> Result<magnus::Value, Error> {
let results = rb_self
fn eval_query(&self, query: String) -> Result<magnus::Value, Error> {
let results = self
.engine
.borrow_mut()
.eval_query(query, false)
.map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {e}")))?;
serde_magnus::serialize(ruby, &results).map_err(|e| {
serde_magnus::serialize(&results).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to serailzie query results: {e}"),
@@ -196,19 +177,15 @@ impl Engine {
})
}
fn eval_rule(
ruby: &Ruby,
rb_self: &Self,
query: String,
) -> Result<Option<magnus::Value>, Error> {
fn eval_rule(&self, query: String) -> Result<Option<magnus::Value>, Error> {
let result =
rb_self.engine.borrow_mut().eval_rule(query).map_err(|e| {
self.engine.borrow_mut().eval_rule(query).map_err(|e| {
Error::new(runtime_error(), format!("Failed to evaluate rule: {e}"))
})?;
match result {
regorus::Value::Undefined => Ok(None),
_ => serde_magnus::serialize(ruby, &result)
regorus::Value::Undefined => Ok(None), // Convert undefined to Ruby's nil
_ => serde_magnus::serialize(&result) // Serialize other results normally
.map(Some)
.map_err(|e| {
magnus::Error::new(
@@ -303,34 +280,6 @@ impl Engine {
})
}
fn set_policy_length_config(
ruby: &Ruby,
rb_self: &Self,
hash: magnus::RHash,
) -> Result<(), Error> {
let spec: PolicyLengthSpec = serde_magnus::deserialize(ruby, hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize policy length config: {e}"),
)
})?;
let config = regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(spec.max_col)
.ok_or_else(|| Error::new(runtime_error(), "max_col must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(spec.max_file_bytes)
.ok_or_else(|| Error::new(runtime_error(), "max_file_bytes must be non-zero"))?,
max_lines: NonZeroUsize::new(spec.max_lines)
.ok_or_else(|| Error::new(runtime_error(), "max_lines must be non-zero"))?,
};
rb_self.engine.borrow_mut().set_policy_length_config(config);
Ok(())
}
fn clear_policy_length_config(&self) -> Result<(), Error> {
self.engine.borrow_mut().clear_policy_length_config();
Ok(())
}
#[cfg(feature = "ast")]
fn get_ast_as_json(&self) -> Result<String, Error> {
self.engine
@@ -412,47 +361,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
engine_class.define_method("set_gather_prints", method!(Engine::set_gather_prints, 1))?;
engine_class.define_method("take_prints", method!(Engine::take_prints, 0))?;
// policy length limits
engine_class.define_method(
"set_policy_length_config",
method!(Engine::set_policy_length_config, 1),
)?;
engine_class.define_method(
"clear_policy_length_config",
method!(Engine::clear_policy_length_config, 0),
)?;
// ast
engine_class.define_method("get_ast_as_json", method!(Engine::get_ast_as_json, 0))?;
// cache configuration (module-level)
#[cfg(feature = "cache")]
{
regorus_module
.define_module_function("set_cache_config", magnus::function!(set_cache_config, 1))?;
regorus_module.define_module_function("clear_cache", magnus::function!(clear_cache, 0))?;
}
Ok(())
}
#[cfg(feature = "cache")]
fn set_cache_config(ruby: &Ruby, hash: magnus::RHash) -> Result<(), Error> {
let spec: CacheConfigSpec = serde_magnus::deserialize(ruby, hash).map_err(|e| {
Error::new(
runtime_error(),
format!("Failed to deserialize cache config: {e}"),
)
})?;
regorus::cache::configure(regorus::cache::Config {
regex: spec.regex,
glob: spec.glob,
});
Ok(())
}
#[cfg(feature = "cache")]
fn clear_cache() -> Result<(), Error> {
regorus::cache::clear();
Ok(())
}

View File

@@ -183,16 +183,6 @@ class TestRegorus < Minitest::Test
assert_equal ["<query.rego>:1: Hello"], @engine.take_prints
end
def test_set_policy_length_config
@engine.set_policy_length_config({ max_col: 2000, max_file_bytes: 1048576, max_lines: 20000 })
@engine.clear_policy_length_config
end
def test_set_cache_config
::Regorus.set_cache_config({ regex: 256, glob: 128 })
::Regorus.clear_cache
end
def alice_results
{
result: [

518
bindings/wasm/Cargo.lock generated
View File

@@ -25,12 +25,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -42,9 +36,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
@@ -63,6 +57,22 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"serde",
"unty",
]
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -80,9 +90,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "borrow-or-share"
@@ -102,9 +112,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.20.2"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "bytecount"
@@ -120,9 +130,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.58"
version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -134,22 +144,11 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -168,30 +167,12 @@ dependencies = [
"phf",
]
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
@@ -218,18 +199,6 @@ dependencies = [
"serde",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -238,9 +207,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fancy-regex"
version = "0.17.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set",
"regex-automata",
@@ -255,27 +224,15 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fluent-uri"
version = "0.4.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
"serde",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -297,25 +254,26 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.32"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-task"
version = "0.3.32"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.32"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -341,27 +299,11 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"r-efi",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasip2",
"wasip3",
"wasm-bindgen",
]
[[package]]
name = "globset"
version = "0.4.18"
@@ -374,31 +316,11 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
@@ -505,12 +427,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -539,59 +455,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
name = "ipnet"
version = "2.12.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "itoa"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.94"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "jsonschema"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
dependencies = [
"ahash",
"base64",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
"regex-syntax",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -601,17 +514,11 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.183"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libm"
@@ -640,17 +547,11 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
[[package]]
name = "memchr"
version = "2.8.0"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minicov"
@@ -762,9 +663,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.4"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
@@ -827,21 +728,15 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.17"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "postcard"
version = "1.1.3"
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"serde",
]
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "potential_utf"
@@ -853,13 +748,12 @@ dependencies = [
]
[[package]]
name = "prettyplease"
version = "0.2.37"
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"proc-macro2",
"syn",
"zerocopy",
]
[[package]]
@@ -873,9 +767,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -887,27 +781,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand"
version = "0.10.0"
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.0"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redox_syscall"
@@ -940,14 +840,13 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.45.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -955,9 +854,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.3"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -967,9 +866,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.14"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -978,15 +877,16 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.10"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "regorus"
version = "0.9.1"
dependencies = [
"anyhow",
"bincode",
"chrono",
"chrono-tz",
"data-encoding",
@@ -995,12 +895,9 @@ dependencies = [
"ipnet",
"jsonschema",
"lazy_static",
"lru",
"msvc_spectre_libs",
"num-bigint",
"num-traits",
"parking_lot",
"postcard",
"rand",
"regex",
"semver",
@@ -1019,10 +916,8 @@ version = "0.9.1"
dependencies = [
"getrandom 0.2.17",
"getrandom 0.3.4",
"getrandom 0.4.2",
"regorus",
"serde",
"serde-wasm-bindgen",
"serde_json",
"uuid",
"wasm-bindgen",
@@ -1037,9 +932,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "same-file"
@@ -1072,17 +967,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -1143,9 +1027,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "slab"
version = "0.4.12"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
@@ -1155,9 +1039,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "spin"
version = "0.10.0"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
@@ -1167,9 +1051,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "syn"
version = "2.0.117"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -1217,23 +1101,11 @@ dependencies = [
"zerovec",
]
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.24"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unsafe-libyaml"
@@ -1241,6 +1113,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.8"
@@ -1261,11 +1139,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
dependencies = [
"getrandom 0.4.2",
"getrandom 0.3.4",
"js-sys",
"rand",
"wasm-bindgen",
@@ -1278,6 +1156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -1318,20 +1197,11 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.117"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -1342,19 +1212,23 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.67"
version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.117"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1362,9 +1236,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.117"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1375,18 +1249,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.117"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.67"
version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941c102b3f0c15b6d72a53205e09e6646aafcf2991e18412cc331dbac1806bc0"
checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d"
dependencies = [
"async-trait",
"cast",
@@ -1406,9 +1280,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.67"
version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26bd6570f39bb1440fd8f01b63461faaf2a3f6078a508e4e54efa99363108d2"
checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2"
dependencies = [
"proc-macro2",
"quote",
@@ -1417,42 +1291,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-shared"
version = "0.2.117"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c29582b14d5bf030b02fa232b9b57faf2afc322d2c61964dd80bad02bf76207"
checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea"
[[package]]
name = "wasm-encoder"
version = "0.244.0"
name = "web-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
"js-sys",
"wasm-bindgen",
]
[[package]]
@@ -1537,88 +1387,6 @@ name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"
@@ -1651,18 +1419,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.47"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.47"
version = "0.8.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
dependencies = [
"proc-macro2",
"quote",
@@ -1725,6 +1493,6 @@ dependencies = [
[[package]]
name = "zmij"
version = "1.0.21"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"

View File

@@ -32,11 +32,9 @@ default = [
"regorus/time",
"regorus/uuid",
"regorus/urlquery",
"regorus/yaml",
"cache"
"regorus/yaml"
]
ast = ["regorus/ast"]
cache = ["regorus/cache"]
coverage = ["regorus/coverage"]
[dependencies]
@@ -44,18 +42,15 @@ regorus = { path = "../..", default-features = false, features = ["arc", "rvm"]
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
wasm-bindgen = "0.2.100"
serde-wasm-bindgen = "0.6"
# Specify uuid as a mandatory dependency so as to enable `js` feature which is now required
# when targeting wasm32-unknown-unknown.
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng", "js"]}
# Configure getrandom for WebAssembly: 0.2 uses the `js` feature, while 0.3+ use `wasm_js`.
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom02 = { package = "getrandom", version = "0.2.15", features = ["std", "js"] }
getrandom03 = { package = "getrandom", version = "0.3.1", features = ["std", "wasm_js"] }
getrandom = { version = "0.4.2", features = ["wasm_js"] }
uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng", "js"]}
# Enable wasm_js. See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
getrandom_for_jsonschema = { package = "getrandom", version = "0.2.15", features = ["std", "js"] }
getrandom = { version = "0.3.1", features = ["std", "wasm_js"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.67"
wasm-bindgen-test = "0.3.40"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }

View File

@@ -3,7 +3,6 @@
#![allow(non_snake_case)]
use core::num::{NonZeroU32, NonZeroUsize};
use regorus::languages::rego::compiler::Compiler;
use regorus::rvm::program::{
generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig,
@@ -27,42 +26,6 @@ struct ModuleSpec {
content: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PolicyLengthSpec {
max_col: u32,
max_file_bytes: usize,
max_lines: usize,
}
#[cfg(feature = "cache")]
#[derive(Deserialize)]
struct CacheConfigSpec {
regex: usize,
glob: usize,
}
/// Configure the global pattern caches used by regex and glob builtins.
///
/// Accepts a JS object: `{ regex, glob }`.
#[cfg(feature = "cache")]
#[wasm_bindgen(js_name = "setCacheConfig")]
pub fn set_cache_config(config: JsValue) -> Result<(), JsValue> {
let spec: CacheConfigSpec = serde_wasm_bindgen::from_value(config).map_err(error_to_jsvalue)?;
regorus::cache::configure(regorus::cache::Config {
regex: spec.regex,
glob: spec.glob,
});
Ok(())
}
/// Clear all entries from every pattern cache.
#[cfg(feature = "cache")]
#[wasm_bindgen(js_name = "clearCache")]
pub fn clear_cache() {
regorus::cache::clear();
}
#[wasm_bindgen]
pub struct Program {
program: Arc<RvmProgram>,
@@ -220,29 +183,6 @@ impl Engine {
self.engine.set_gather_prints(b)
}
/// Set the policy length limits used when loading policies.
///
/// Accepts a JS object: `{ maxCol, maxFileBytes, maxLines }`.
pub fn setPolicyLengthConfig(&mut self, config: JsValue) -> Result<(), JsValue> {
let spec: PolicyLengthSpec =
serde_wasm_bindgen::from_value(config).map_err(error_to_jsvalue)?;
self.engine
.set_policy_length_config(regorus::PolicyLengthConfig {
max_col: NonZeroU32::new(spec.max_col)
.ok_or_else(|| JsValue::from_str("maxCol must be non-zero"))?,
max_file_bytes: NonZeroUsize::new(spec.max_file_bytes)
.ok_or_else(|| JsValue::from_str("maxFileBytes must be non-zero"))?,
max_lines: NonZeroUsize::new(spec.max_lines)
.ok_or_else(|| JsValue::from_str("maxLines must be non-zero"))?,
});
Ok(())
}
/// Clear the policy length configuration, reverting to defaults.
pub fn clearPolicyLengthConfig(&mut self) {
self.engine.clear_policy_length_config();
}
/// Take the gathered output of print statements.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints

View File

@@ -3,18 +3,12 @@
var regorus = require('./pkg/regorusjs');
// Configure the global pattern caches.
regorus.setCacheConfig({ regex: 256, glob: 128 });
// Create an engine.
var engine = new regorus.Engine();
// Enable code coverage
engine.setEnableCoverage(true);
// Raise the default col limit to 2000
engine.setPolicyLengthConfig({ maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000 });
// Add Rego policy.
var pkg = engine.addPolicy(
// Associate this file name with policy

View File

@@ -1,77 +0,0 @@
(* Azure Policy grammar.
*
* All key matching is case-insensitive. JSON object keys are unordered,
* so the ordering shown below is for readability only.
*)
(* ================================================================
* Policy rule & then block
* NOTE: Keys may appear in any order; extra keys may appear between
* the recognized ones. The ordering below is illustrative.
* ================================================================ *)
policy-rule ::= '{' '"if"' ':' constraint ',' '"then"' ':' then-block
(',' STRING ':' json-value)* '}'
then-block ::= '{' '"effect"' ':' STRING
(',' '"details"' ':' json-value)? '}'
(* ================================================================
* Constraints
* ================================================================ *)
constraint ::= allOf | anyOf | not | condition
allOf ::= '{' '"allOf"' ':' '[' (constraint (',' constraint)*)? ']' '}'
anyOf ::= '{' '"anyOf"' ':' '[' (constraint (',' constraint)*)? ']' '}'
not ::= '{' '"not"' ':' constraint '}'
(* Keys within a condition are unordered; exactly one lhs-entry and one
* op-entry are required. *)
condition ::= '{' lhs-entry ',' op-entry '}'
lhs-entry ::= field | value-lhs | count
field ::= '"field"' ':' string-value
value-lhs ::= '"value"' ':' json-value
op-entry ::= operator ':' json-value
operator ::= '"contains"' | '"containsKey"' | '"equals"' | '"notEquals"'
| '"greater"' | '"greaterOrEquals"' | '"less"' | '"lessOrEquals"'
| '"exists"' | '"in"' | '"notIn"'
| '"like"' | '"notLike"'
| '"match"' | '"matchInsensitively"'
| '"notMatch"' | '"notMatchInsensitively"'
| '"notContains"' | '"notContainsKey"'
(* ================================================================
* Count expressions
* ================================================================ *)
count ::= '"count"' ':' count-inner
count-inner ::= count-field | count-value
count-field ::= '{' field (',' where)? '}'
count-value ::= '{' value-lhs (',' '"name"' ':' STRING)? (',' where)? '}'
where ::= '"where"' ':' constraint
(* ================================================================
* JSON values & template expressions
* ================================================================ *)
string-value ::= STRING | '"[' string-expr ']"'
json-value ::= STRING | NUMBER | BOOL | NULL
| array | object
| '"[' string-expr ']"'
array ::= '[' (json-value (',' json-value)*)? ']'
object ::= '{' (STRING ':' json-value (',' STRING ':' json-value)*)? '}'
(* ================================================================
* ARM template expression sub-grammar
* ================================================================ *)
string-expr ::= NUMBER | STRING | '-' string-expr | complex-expr
complex-expr ::= IDENT
| complex-expr '.' IDENT
| complex-expr '(' (string-expr (',' string-expr)*)? ')'
| complex-expr '[' string-expr ']'

View File

@@ -90,11 +90,11 @@ version:
`3`).
2. **Section manifest**: four little-endian `u32` lengths for entry points,
sources, literals, and the rule tree, plus a single-byte `rego_v0` flag.
3. **Preamble payloads**: each section is encoded with `postcard` using helper
3. **Preamble payloads**: each section is encoded with `bincode` using helper
wrappers (`BinaryValueSlice`, `BinaryValueRef`) to stream complex `Value`
graphs without cloning.
4. **Program core**: the remaining `Program` struct is serialized once more via
`postcard`; fields skipped by serde (entry points, literals, sources,
`bincode`; fields skipped by serde (entry points, literals, sources,
rule_tree, resolved builtins) are re-inserted from the preamble when the
program is reconstructed.

View File

@@ -1,25 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#[cfg(not(any(target_family = "wasm", miri)))]
#[cfg(not(any(target_family = "wasm")))]
pub mod mimalloc;
#[cfg(feature = "allocator-memory-limits")]
#[cfg(not(any(target_family = "wasm", miri)))]
#[cfg(not(any(target_family = "wasm")))]
pub use mimalloc::{
allocation_stats_snapshot, current_thread_allocation_stats, global_allocation_stats_snapshot,
GlobalAllocationStats, ThreadAllocationStats,
};
#[cfg(feature = "allocator-memory-limits")]
#[cfg(not(any(target_family = "wasm", miri)))]
#[cfg(not(any(target_family = "wasm")))]
pub mod limits;
/// Declare a global allocator if the platform supports it.
#[macro_export]
macro_rules! assign_global {
() => {
#[cfg(not(any(target_family = "wasm", miri)))]
#[cfg(not(any(target_family = "wasm")))]
#[global_allocator]
static GLOBAL: mimalloc::mimalloc::Mimalloc = mimalloc::mimalloc::Mimalloc;
};

View File

@@ -1,151 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Shared helpers: type coercion, comparison, pattern matching, and path resolution.
#![deny(
clippy::arithmetic_side_effects,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::shadow_unrelated,
clippy::unwrap_used,
clippy::missing_const_for_fn,
clippy::option_if_let_else,
clippy::semicolon_if_nothing_returned,
clippy::useless_let_if_seq
)]
use crate::languages::azure_policy::strings;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
// ── Type helpers ──────────────────────────────────────────────────────
pub const fn is_true(value: &Value) -> bool {
matches!(value, Value::Bool(true))
}
pub const fn is_undefined(value: &Value) -> bool {
matches!(value, Value::Undefined)
}
pub fn as_string(value: &Value) -> Option<String> {
match *value {
Value::String(ref s) => Some(s.to_string()),
_ => None,
}
}
/// Borrow the inner string of a `Value::String` without cloning.
pub fn as_str(value: &Value) -> Option<&str> {
match *value {
Value::String(ref s) => Some(s),
_ => None,
}
}
/// Try to parse a string as a number for Azure Policy type coercion.
pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
use core::str::FromStr as _;
// Try integer first, then float.
i64::from_str(s.trim())
.map(crate::number::Number::from)
.ok()
.or_else(|| {
f64::from_str(s.trim())
.map(crate::number::Number::from)
.ok()
})
}
// ── Path resolution ───────────────────────────────────────────────────
pub fn resolve_path(root: &Value, path: &str) -> Value {
let segments = tokenize_path(path);
let mut current = root.clone();
for segment in segments {
#[allow(clippy::pattern_type_mismatch)]
match &current {
Value::Object(map) => {
let mut next = None;
for (key, value) in map.iter() {
if let Value::String(ref key_str) = *key {
if strings::keys::eq(key_str, &segment) {
next = Some(value.clone());
break;
}
}
}
if let Some(value) = next {
current = value;
} else {
return Value::Undefined;
}
}
Value::Array(items) => {
let Ok(index) = segment.parse::<usize>() else {
return Value::Undefined;
};
let Some(value) = items.get(index) else {
return Value::Undefined;
};
current = value.clone();
}
_ => return Value::Undefined,
}
}
current
}
fn tokenize_path(path: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut token = String::new();
let mut bracket = String::new();
let mut in_bracket = false;
for ch in path.chars() {
match ch {
'.' if !in_bracket => {
if !token.is_empty() {
segments.push(token.clone());
token.clear();
}
}
'[' => {
in_bracket = true;
if !token.is_empty() {
segments.push(token.clone());
token.clear();
}
}
']' => {
in_bracket = false;
let cleaned = bracket.trim_matches('"').trim_matches('\'').to_string();
if !cleaned.is_empty() {
segments.push(cleaned);
}
bracket.clear();
}
_ => {
if in_bracket {
bracket.push(ch);
} else {
token.push(ch);
}
}
}
}
if !token.is_empty() {
segments.push(token);
}
segments
}

View File

@@ -1,65 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Azure Policy builtins: operators, logic functions, and ARM template functions.
#![deny(
clippy::arithmetic_side_effects,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::shadow_unrelated,
clippy::unwrap_used,
clippy::missing_const_for_fn,
clippy::option_if_let_else,
clippy::semicolon_if_nothing_returned,
clippy::useless_let_if_seq
)]
pub mod helpers;
mod operators;
mod template_functions;
mod template_functions_collection;
mod template_functions_datetime;
mod template_functions_encoding;
mod template_functions_misc;
mod template_functions_numeric;
mod template_functions_string;
use crate::builtins;
/// Upper bound on the number of arguments accepted by variadic builtins.
///
/// ARM template expressions can pass many arguments to functions like
/// `min`, `max`, `union`, `intersection`, `format`, `createObject`, etc.
/// We register them with this cap instead of 0 so that the compiler/VM
/// arity checks accept real call sites.
pub(super) const MAX_VARIADIC_ARGS: u8 = 64;
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
// Logic functions
m.insert(
"azure.policy.logic_all",
(operators::logic_all, MAX_VARIADIC_ARGS),
);
m.insert(
"azure.policy.logic_any",
(operators::logic_any, MAX_VARIADIC_ARGS),
);
m.insert("azure.policy.if", (operators::if_fn, 3));
// Field resolution
m.insert("azure.policy.resolve_field", (operators::resolve_field, 2));
// Parameter resolution with default-value fallback
m.insert("azure.policy.get_parameter", (operators::get_parameter, 3));
// ARM template functions
template_functions::register(m);
template_functions_string::register(m);
template_functions_encoding::register(m);
template_functions_collection::register(m);
template_functions_numeric::register(m);
template_functions_datetime::register(m);
template_functions_misc::register(m);
}

View File

@@ -1,116 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Azure Policy utility builtins: parameter resolution, field resolution,
//! logic_all (for ARM `and()`), and if().
//!
//! The 20 condition operators (equals, notEquals, greater, …, exists) and
//! the logic_not / logic_any combinators are now compiled as first-class
//! RVM instructions (`PolicyEquals`, `PolicyNot`, `AllOfStart`/`AnyOfStart`,
//! etc.) and no longer go through the builtin dispatch path.
use crate::ast::{Expr, Ref};
use crate::lexer::Span;
use crate::value::Value;
use anyhow::Result;
use super::helpers::{as_string, is_true, is_undefined, resolve_path};
// ── Parameter resolution ──────────────────────────────────────────────
/// `azure.policy.get_parameter(params, defaults, name)`
///
/// Returns `params[name]` if it exists and is not undefined; otherwise
/// falls back to `defaults[name]`. This lets the compiler bake parameter
/// default values into the program's literal table while still allowing
/// callers to override them via `input.parameters`.
pub(super) fn get_parameter(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [params_obj, defaults_obj, name] = args
else {
return Ok(Value::Undefined);
};
// Try caller-supplied parameters first.
let val = &params_obj[name];
if !is_undefined(val) {
return Ok(val.clone());
}
// Fall back to compiled-in defaults.
Ok(defaults_obj[name].clone())
}
// ── Field resolution ──────────────────────────────────────────────────
pub(super) fn resolve_field(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [resource, field_path] = args
else {
return Ok(Value::Undefined);
};
let Some(path) = as_string(field_path) else {
return Ok(Value::Undefined);
};
Ok(resolve_path(resource, &path))
}
// ── Logic functions ───────────────────────────────────────────────────
/// `azure.policy.logic_all(a, b, ...)`
///
/// Used by the ARM template `and()` function. Returns true iff every
/// argument is `Bool(true)`.
pub(super) fn logic_all(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
Ok(Value::Bool(args.iter().all(is_true)))
}
/// `azure.policy.logic_any(a, b, ...)`
///
/// Used by the ARM template `or()` function. Returns true iff any
/// argument is `Bool(true)`.
pub(super) fn logic_any(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
Ok(Value::Bool(args.iter().any(is_true)))
}
/// `azure.policy.if(cond, when_true, when_false)`
pub(super) fn if_fn(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [cond, when_true, when_false] = args
else {
return Ok(Value::Undefined);
};
if is_true(cond) {
Ok(when_true.clone())
} else {
Ok(when_false.clone())
}
}

View File

@@ -1,372 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template function builtins for Azure Policy expressions.
//!
//! Implements: split, empty, first, last, startsWith,
//! endsWith, int, string, bool, padLeft, ipRangeContains.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::languages::azure_policy::strings::case_fold;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use anyhow::Result;
use core::net::IpAddr;
use ipnet::IpNet;
use super::helpers::{as_str, try_coerce_to_number};
/// Truncate `f64` to `i64` (saturating semantics since Rust 1.45).
///
/// The standard library provides no `TryFrom<f64>` for `i64`, so a raw `as`
/// cast is the only option. Wrapping it in a named function keeps the rest
/// of the module free of `clippy::as_conversions` warnings.
#[expect(clippy::as_conversions, reason = "no TryFrom<f64> for i64 in std")]
const fn truncate_f64_to_i64(f: f64) -> i64 {
f as i64
}
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.split", (fn_split, 2));
m.insert("azure.policy.fn.empty", (fn_empty, 1));
m.insert("azure.policy.fn.first", (fn_first, 1));
m.insert("azure.policy.fn.last", (fn_last, 1));
m.insert("azure.policy.fn.starts_with", (fn_starts_with, 2));
m.insert("azure.policy.fn.ends_with", (fn_ends_with, 2));
m.insert("azure.policy.fn.int", (fn_int, 1));
m.insert("azure.policy.fn.string", (fn_string, 1));
m.insert("azure.policy.fn.bool", (fn_bool, 1));
m.insert("azure.policy.fn.pad_left", (fn_pad_left, 3));
m.insert(
"azure.policy.fn.ip_range_contains",
(fn_ip_range_contains, 2),
);
}
/// `split(inputString, delimiter)` → array of strings.
fn fn_split(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [input_val, delim_val] = args
else {
return Ok(Value::Undefined);
};
let Some(input) = as_str(input_val) else {
return Ok(Value::Undefined);
};
// The delimiter argument can be a single string or an array of strings.
// When an array is provided, the input is split on ANY of the delimiters.
match *delim_val {
Value::String(ref delimiter) => {
if delimiter.is_empty() {
// Empty delimiter → no split; return input as single-element array.
return Ok(Value::from(alloc::vec![Value::from(input)]));
}
let parts: alloc::vec::Vec<Value> = input
.split(delimiter.as_ref())
.map(|s| Value::from(s.to_string()))
.collect();
Ok(Value::from(parts))
}
Value::Array(ref delimiters) => {
// Collect all string delimiters from the array.
let delims: alloc::vec::Vec<&str> = delimiters
.iter()
.filter_map(|v| match *v {
Value::String(ref s) => Some(s.as_ref()),
_ => None,
})
.collect();
if delims.is_empty() {
// No valid delimiters — return input as single-element array.
return Ok(Value::from(alloc::vec![Value::from(input)]));
}
// Scan the input and split on any matching delimiter.
// At each position, try delimiters longest-first to avoid
// substring-overlap issues.
let mut sorted_delims = delims.clone();
sorted_delims.sort_by_key(|d| core::cmp::Reverse(d.len()));
let mut parts = alloc::vec::Vec::new();
let mut current = String::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
let mut matched = false;
for &d in &sorted_delims {
if !d.is_empty() && bytes.get(i..).is_some_and(|b| b.starts_with(d.as_bytes()))
{
parts.push(Value::from(core::mem::take(&mut current)));
i = i.wrapping_add(d.len());
matched = true;
break;
}
}
if !matched {
// Safe: we iterate byte-by-byte only when no delimiter matched.
// For correctness with multi-byte UTF-8, advance one char.
if let Some(ch) = input.get(i..).and_then(|s| s.chars().next()) {
current.push(ch);
i = i.wrapping_add(ch.len_utf8());
} else {
i = i.wrapping_add(1);
}
}
}
parts.push(Value::from(current));
Ok(Value::from(parts))
}
_ => Ok(Value::Undefined),
}
}
/// `empty(item)` → true if string/array/object is empty or value is null/undefined.
fn fn_empty(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Bool(true));
};
let result = match *arg {
Value::String(ref s) => s.is_empty(),
Value::Array(ref a) => a.is_empty(),
Value::Object(ref o) => o.is_empty(),
Value::Null | Value::Undefined => true,
_ => false,
};
Ok(Value::Bool(result))
}
/// `first(arg)` → first element of array or first character of string.
///
/// Azure semantics: first of an empty string returns empty string,
/// first of an empty array returns null.
fn fn_first(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Array(ref a) => Ok(a.first().cloned().unwrap_or(Value::Null)),
Value::String(ref s) => Ok(s
.chars()
.next()
.map_or_else(|| Value::from(""), |ch| Value::from(ch.to_string()))),
_ => Ok(Value::Undefined),
}
}
/// `last(arg)` → last element of array or last character of string.
///
/// Azure semantics: last of an empty string returns empty string,
/// last of an empty array returns null.
fn fn_last(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Array(ref a) => Ok(a.last().cloned().unwrap_or(Value::Null)),
Value::String(ref s) => Ok(s
.chars()
.last()
.map_or_else(|| Value::from(""), |ch| Value::from(ch.to_string()))),
_ => Ok(Value::Undefined),
}
}
/// `startsWith(stringToSearch, stringToFind)` → bool (case-insensitive).
///
/// Uses full Unicode case folding via ICU4X.
fn fn_starts_with(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Bool(false));
};
let fh = case_fold::fold(haystack);
let fn_ = case_fold::fold(needle);
Ok(Value::Bool(fh.starts_with(&*fn_)))
}
/// `endsWith(stringToSearch, stringToFind)` → bool (case-insensitive).
///
/// Uses full Unicode case folding via ICU4X.
fn fn_ends_with(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Bool(false));
};
let fh = case_fold::fold(haystack);
let fn_ = case_fold::fold(needle);
Ok(Value::Bool(fh.ends_with(&*fn_)))
}
/// `int(valueToConvert)` → integer number.
fn fn_int(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Number(ref n) => {
// Truncate to integer.
n.as_i64()
.map(Value::from)
.or_else(|| n.as_f64().map(|f| Value::from(truncate_f64_to_i64(f))))
.map_or(Ok(Value::Undefined), Ok)
}
Value::String(ref s) => try_coerce_to_number(s).map_or(Ok(Value::Undefined), |n| {
n.as_i64()
.map(Value::from)
.or_else(|| n.as_f64().map(|f| Value::from(truncate_f64_to_i64(f))))
.map_or(Ok(Value::Undefined), Ok)
}),
_ => Ok(Value::Undefined),
}
}
/// `string(valueToConvert)` → string representation.
fn fn_string(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::String(_) => Ok(arg.clone()),
Value::Bool(b) => Ok(Value::from(b.to_string())),
Value::Number(ref n) => Ok(Value::from(n.format_decimal())),
Value::Null => Ok(Value::from("null")),
Value::Undefined => Ok(Value::Undefined),
// For arrays and objects, produce JSON-style representation.
_ => Ok(Value::from(arg.to_string())),
}
}
/// `bool(value)` → boolean.
fn fn_bool(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Bool(_) => Ok(arg.clone()),
Value::String(ref s) => match s.to_lowercase().as_str() {
"true" | "1" => Ok(Value::Bool(true)),
"false" | "0" => Ok(Value::Bool(false)),
_ => Ok(Value::Undefined),
},
Value::Number(ref n) => n
.as_i64()
.map(|i| Value::Bool(i != 0))
.or_else(|| n.as_f64().map(|f| Value::Bool(f != 0.0)))
.map_or(Ok(Value::Undefined), Ok),
_ => Ok(Value::Undefined),
}
}
/// `padLeft(value, totalWidth, padChar)` → left-padded string.
fn fn_pad_left(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
if args.len() < 2 || args.len() > 3 {
return Ok(Value::Undefined);
}
let Some(value) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let width = args.get(1).and_then(|width_val| match *width_val {
Value::Number(ref n) => n
.as_i64()
.and_then(|x| usize::try_from(x).ok())
.or_else(|| {
n.as_f64()
.and_then(|x| usize::try_from(truncate_f64_to_i64(x)).ok())
}),
Value::String(ref s) => try_coerce_to_number(s).and_then(|n| {
n.as_i64()
.and_then(|x| usize::try_from(x).ok())
.or_else(|| {
n.as_f64()
.and_then(|x| usize::try_from(truncate_f64_to_i64(x)).ok())
})
}),
_ => None,
});
let Some(total_width) = width else {
return Ok(Value::Undefined);
};
let pad_char = args
.get(2)
.and_then(as_str)
.and_then(|s| s.chars().next())
.unwrap_or(' ');
let value_len = value.chars().count();
if value_len >= total_width {
return Ok(Value::from(value));
}
let pad_count = total_width.saturating_sub(value_len);
let mut padded = String::with_capacity(total_width);
for _ in 0..pad_count {
padded.push(pad_char);
}
padded.push_str(value);
Ok(Value::from(padded))
}
/// `ipRangeContains(range, targetRange)` → bool.
fn fn_ip_range_contains(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [range_val, target_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(range), Some(target)) = (as_str(range_val), as_str(target_val)) else {
return Ok(Value::Bool(false));
};
let Ok(net) = range.parse::<IpNet>() else {
return Ok(Value::Bool(false));
};
if target.contains('/') {
let Ok(target_net) = target.parse::<IpNet>() else {
return Ok(Value::Bool(false));
};
return Ok(Value::Bool(net.contains(&target_net)));
}
let Ok(target_ip) = target.parse::<IpAddr>() else {
return Ok(Value::Bool(false));
};
Ok(Value::Bool(net.contains(&target_ip)))
}

View File

@@ -1,317 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template collection function builtins for Azure Policy expressions.
//!
//! Implements: intersection, union, take, skip, range, array, coalesce, createObject.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use anyhow::Result;
use super::helpers::is_undefined;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert(
"azure.policy.fn.intersection",
(fn_intersection, super::MAX_VARIADIC_ARGS),
);
m.insert(
"azure.policy.fn.union",
(fn_union, super::MAX_VARIADIC_ARGS),
);
m.insert("azure.policy.fn.take", (fn_take, 2));
m.insert("azure.policy.fn.skip", (fn_skip, 2));
m.insert("azure.policy.fn.range", (fn_range, 2));
m.insert("azure.policy.fn.array", (fn_array, 1));
m.insert(
"azure.policy.fn.coalesce",
(fn_coalesce, super::MAX_VARIADIC_ARGS),
);
m.insert(
"azure.policy.fn.create_object",
(fn_create_object, super::MAX_VARIADIC_ARGS),
);
}
/// `intersection(arg1, arg2, ...)` → elements common to all arrays, or keys common
/// to all objects.
///
/// For arrays: returns elements present in every input array.
/// For objects: returns keys (with values from the first) present in every input.
fn fn_intersection(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
let rest = args.get(1..).unwrap_or_default();
match *first {
Value::Array(ref first) => {
// Intersection of arrays: keep elements from first that appear in all others.
let mut result: Vec<Value> = first.as_ref().clone();
for arg in rest {
let Value::Array(ref other) = *arg else {
return Ok(Value::Undefined);
};
result.retain(|item| other.contains(item));
}
Ok(Value::from(result))
}
Value::Object(ref first) => {
// Intersection of objects: keep key-value pairs from the first
// object only when the key exists in every other object AND
// the value is equal across all of them.
let mut result: BTreeMap<Value, Value> = first.as_ref().clone();
for arg in rest {
let Value::Object(ref other) = *arg else {
return Ok(Value::Undefined);
};
result.retain(|k, v| other.get(k).is_some_and(|ov| *ov == *v));
}
Ok(Value::Object(Rc::new(result)))
}
_ => Ok(Value::Undefined),
}
}
/// `union(arg1, arg2, ...)` → all unique elements from arrays, or merged objects.
///
/// For arrays: returns distinct elements across all arrays.
/// For objects: merges all objects (later values overwrite earlier for same key).
fn fn_union(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
match *first {
Value::Array(_) => {
// Union of arrays: collect unique elements preserving first-seen order.
let mut seen = alloc::collections::BTreeSet::<&Value>::new();
let mut result = Vec::new();
for arg in args {
let Value::Array(ref arr) = *arg else {
return Ok(Value::Undefined);
};
for item in arr.iter() {
if seen.insert(item) {
result.push(item.clone());
}
}
}
Ok(Value::from(result))
}
Value::Object(_) => {
// Union of objects: recursive merge. Nested objects are merged
// recursively; all other types (including arrays) use last-writer-wins.
let mut result = BTreeMap::<Value, Value>::new();
for arg in args {
let Value::Object(ref obj) = *arg else {
return Ok(Value::Undefined);
};
for (k, v) in obj.iter() {
#[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => {
merge_objects(prev, next)
}
_ => v.clone(),
};
result.insert(k.clone(), merged);
}
}
Ok(Value::Object(Rc::new(result)))
}
_ => Ok(Value::Undefined),
}
}
/// `take(originalValue, numberToTake)` → first N elements of array or chars of string.
fn fn_take(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [original, count_val] = args
else {
return Ok(Value::Undefined);
};
let count = extract_usize(count_val).unwrap_or(0);
match *original {
Value::Array(ref arr) => {
let n = count.min(arr.len());
Ok(Value::from(arr.get(..n).unwrap_or_default().to_vec()))
}
Value::String(ref s) => {
let taken: alloc::string::String = s.chars().take(count).collect();
Ok(Value::from(taken))
}
_ => Ok(Value::Undefined),
}
}
/// `skip(originalValue, numberToSkip)` → array/string after skipping N elements.
fn fn_skip(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [original, count_val] = args
else {
return Ok(Value::Undefined);
};
let count = extract_usize(count_val).unwrap_or(0);
match *original {
Value::Array(ref arr) => {
let n = count.min(arr.len());
Ok(Value::from(arr.get(n..).unwrap_or_default().to_vec()))
}
Value::String(ref s) => {
let skipped: alloc::string::String = s.chars().skip(count).collect();
Ok(Value::from(skipped))
}
_ => Ok(Value::Undefined),
}
}
/// `range(startIndex, count)` → array of integers starting at startIndex.
///
/// Azure limits: count ≤ 10000, startIndex + count ≤ 2147483647.
fn fn_range(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [start_val, count_val] = args
else {
return Ok(Value::Undefined);
};
let (Some(start), Some(count)) = (extract_i64(start_val), extract_i64(count_val)) else {
return Ok(Value::Undefined);
};
if count < 0 {
return Ok(Value::Undefined);
}
// Enforce Azure-documented limits.
if count > 10_000 {
anyhow::bail!("range: count ({count}) exceeds maximum of 10000");
}
let end = start
.checked_add(count)
.ok_or_else(|| anyhow::anyhow!("range overflow"))?;
if end > 2_147_483_647 {
anyhow::bail!("range: startIndex + count ({end}) exceeds maximum of 2147483647");
}
let mut result = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
for i in 0..count {
let val = start
.checked_add(i)
.ok_or_else(|| anyhow::anyhow!("range overflow"))?;
result.push(Value::from(val));
}
Ok(Value::from(result))
}
/// `array(convertToArray)` → wraps a single value in an array.
///
/// If the input is already an array, returns it as-is.
fn fn_array(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::from(Vec::<Value>::new()));
};
match *arg {
Value::Array(_) => Ok(arg.clone()),
_ => Ok(Value::from(alloc::vec![arg.clone()])),
}
}
/// `coalesce(arg1, arg2, ...)` → first non-null, non-undefined argument.
fn fn_coalesce(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
for arg in args {
if !is_undefined(arg) && !matches!(arg, Value::Null) {
return Ok(arg.clone());
}
}
Ok(Value::Null)
}
/// `createObject(key1, value1, key2, value2, ...)` → object from key-value pairs.
fn fn_create_object(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
if !args.len().is_multiple_of(2) {
anyhow::bail!(
"createObject: expected an even number of arguments (key-value pairs), \
but received {}",
args.len()
);
}
let mut map = BTreeMap::<Value, Value>::new();
for pair in args.chunks(2) {
#[allow(clippy::pattern_type_mismatch)]
if let [key, value] = pair {
map.insert(key.clone(), value.clone());
}
}
Ok(Value::Object(Rc::new(map)))
}
// ── Helpers ───────────────────────────────────────────────────────────
/// Recursively merge two objects. Nested objects are merged; everything
/// else (including arrays) uses the value from `incoming`.
fn merge_objects(base: &BTreeMap<Value, Value>, overlay: &BTreeMap<Value, Value>) -> Value {
let mut result = base.clone();
for (k, v) in overlay {
#[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => merge_objects(prev, next),
_ => v.clone(),
};
result.insert(k.clone(), merged);
}
Value::Object(Rc::new(result))
}
fn extract_usize(v: &Value) -> Option<usize> {
match *v {
Value::Number(ref n) => n
.as_i64()
.and_then(|x| usize::try_from(x).ok())
.or_else(|| n.as_f64().and_then(|x| usize::try_from(f64_as_i64(x)).ok())),
_ => None,
}
}
fn extract_i64(v: &Value) -> Option<i64> {
match *v {
Value::Number(ref n) => n.as_i64().or_else(|| n.as_f64().map(f64_as_i64)),
_ => None,
}
}
/// Deliberate truncating conversion from `f64` → `i64`.
#[expect(clippy::as_conversions)]
const fn f64_as_i64(x: f64) -> i64 {
x as i64
}

View File

@@ -1,687 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template date/time builtins: dateTimeAdd, dateTimeFromEpoch,
//! dateTimeToEpoch, addDays.
//!
//! `utcNow()` is handled in the compiler (loaded from context), not here.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use core::fmt::Write as _;
use chrono::{DateTime, Duration, FixedOffset, Utc};
use super::helpers::as_str;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.date_time_add", (fn_date_time_add, 3));
m.insert(
"azure.policy.fn.date_time_from_epoch",
(fn_date_time_from_epoch, 1),
);
m.insert(
"azure.policy.fn.date_time_to_epoch",
(fn_date_time_to_epoch, 1),
);
m.insert("azure.policy.fn.add_days", (fn_add_days, 0));
}
// ── ISO 8601 datetime parsing ─────────────────────────────────────────
/// Parse an ISO 8601 / RFC 3339 datetime string.
fn parse_datetime(s: &str) -> Option<DateTime<FixedOffset>> {
parse_datetime_styled(s).map(|(dt, _)| dt)
}
/// The detected format style of a parsed datetime string, used to reproduce
/// the same shape when no explicit output format is given.
#[derive(Clone, Copy)]
enum DateTimeStyle {
/// RFC 3339 with T separator and Z suffix.
Rfc3339Z,
/// RFC 3339 with T separator and explicit numeric offset.
Rfc3339Offset,
/// T separator, no timezone (assumed UTC).
IsoNoTz,
/// Space separator, no timezone (assumed UTC).
SpaceNoTz,
/// Space separator with Z suffix.
SpaceZ,
/// Space separator with explicit offset.
SpaceOffset,
}
/// Parse a datetime string and return both the parsed value and the detected
/// input style so that output formatting can preserve it.
fn parse_datetime_styled(s: &str) -> Option<(DateTime<FixedOffset>, DateTimeStyle)> {
// Check for space separator at position 10 (after "YYYY-MM-DD") so that
// space-separated inputs are detected before RFC 3339 (which also allows
// a space in place of T).
if s.len() > 10 && s.as_bytes().get(10).copied() == Some(b' ') {
// Space separator with explicit offset (e.g. "2020-04-07 14:55:59+00:00").
if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%:z") {
return Some((dt, DateTimeStyle::SpaceOffset));
}
if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%:z") {
return Some((dt, DateTimeStyle::SpaceOffset));
}
// Space separator with Z suffix (e.g. "2020-04-07 14:55:59Z").
if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(stripped, "%Y-%m-%d %H:%M:%S")
{
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::SpaceZ));
}
if let Ok(naive) =
chrono::NaiveDateTime::parse_from_str(stripped, "%Y-%m-%d %H:%M:%S%.f")
{
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::SpaceZ));
}
}
// Space separator, no timezone (assume UTC).
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::SpaceNoTz));
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::SpaceNoTz));
}
}
// Try RFC 3339 first (most common for ARM templates).
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
let style = if s.ends_with('Z') || s.ends_with('z') {
DateTimeStyle::Rfc3339Z
} else {
DateTimeStyle::Rfc3339Offset
};
return Some((dt, style));
}
// Try with T separator, no timezone (assume UTC).
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::IsoNoTz));
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some((utc.fixed_offset(), DateTimeStyle::IsoNoTz));
}
None
}
/// Format a datetime as ISO 8601 string. UTC datetimes use the `Z` suffix
/// (matching Azure's documented output), while offset datetimes keep their
/// explicit offset. Fractional seconds are included when non-zero.
fn format_datetime(dt: &DateTime<FixedOffset>) -> String {
if dt.offset().local_minus_utc() == 0 {
// UTC → use Z suffix
dt.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string()
} else {
dt.format("%Y-%m-%dT%H:%M:%S%.f%:z").to_string()
}
}
/// Format a datetime preserving the detected input style.
fn format_datetime_styled(dt: &DateTime<FixedOffset>, style: DateTimeStyle) -> String {
match style {
DateTimeStyle::Rfc3339Z => dt.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string(),
DateTimeStyle::Rfc3339Offset => dt.format("%Y-%m-%dT%H:%M:%S%.f%:z").to_string(),
DateTimeStyle::IsoNoTz => dt.format("%Y-%m-%dT%H:%M:%S%.f").to_string(),
DateTimeStyle::SpaceNoTz => dt.format("%Y-%m-%d %H:%M:%S%.f").to_string(),
DateTimeStyle::SpaceZ => dt.format("%Y-%m-%d %H:%M:%S%.fZ").to_string(),
DateTimeStyle::SpaceOffset => dt.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string(),
}
}
// ── ISO 8601 duration parsing ─────────────────────────────────────────
/// Parse an ISO 8601 duration string into a `chrono::Duration`.
///
/// Supports: `P[nY][nM][nD][T[nH][nM][nS]]`
/// Examples: `P1D`, `PT1H`, `P1Y2M3DT4H5M6S`, `PT30M`, `-P1D`
///
/// Note: months/years are approximated (1 month = 30 days, 1 year = 365 days)
/// since chrono::Duration is absolute. ARM template behavior matches this.
fn parse_iso8601_duration(s: &str) -> Option<Duration> {
let (s, negative) = s.strip_prefix('-').map_or((s, false), |rest| (rest, true));
let s = s.strip_prefix('P')?;
let mut total_seconds: i64 = 0;
let mut in_time = false;
let mut num_buf = String::new();
for ch in s.chars() {
match ch {
'T' => {
if !num_buf.is_empty() {
return None;
}
in_time = true;
}
'0'..='9' | '.' => {
num_buf.push(ch);
}
'Y' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 365.0 * 86400.0))?;
num_buf.clear();
}
'M' if !in_time => {
// Months in date part
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 30.0 * 86400.0))?;
num_buf.clear();
}
'W' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 7.0 * 86400.0))?;
num_buf.clear();
}
'D' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 86400.0))?;
num_buf.clear();
}
'H' if in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 3600.0))?;
num_buf.clear();
}
'M' if in_time => {
// Minutes in time part
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 60.0))?;
num_buf.clear();
}
'S' if in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n))?;
num_buf.clear();
}
_ => return None,
}
}
if !num_buf.is_empty() {
return None;
}
let dur = Duration::seconds(if negative {
total_seconds.checked_neg()?
} else {
total_seconds
});
Some(dur)
}
// ── Builtin functions ─────────────────────────────────────────────────
/// `dateTimeAdd(base, duration, format?)` → add ISO 8601 duration to datetime.
///
/// ARM template: `dateTimeAdd('2020-04-07 14:55:59', 'P3Y2M', 'yyyy-MM-dd')`
/// The optional third argument is a .NET-style custom date/time format string.
/// When absent, the output uses the same format as the input base string.
fn fn_date_time_add(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(base_str) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(duration_str) = args.get(1).and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some((base_dt, style)) = parse_datetime_styled(base_str) else {
return Ok(Value::Undefined);
};
let Some(duration) = parse_iso8601_duration(duration_str) else {
return Ok(Value::Undefined);
};
let result = base_dt
.checked_add_signed(duration)
.ok_or_else(|| anyhow::anyhow!("dateTimeAdd: datetime overflow"))?;
let output = match args.get(2).and_then(as_str) {
Some(fmt) => format_datetime_dotnet(&result, fmt)?,
None => format_datetime_styled(&result, style),
};
Ok(Value::from(output))
}
/// `dateTimeFromEpoch(epoch)` → ISO 8601 UTC datetime string from Unix epoch.
fn fn_date_time_from_epoch(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(epoch) = args.first().and_then(extract_i64) else {
return Ok(Value::Undefined);
};
let Some(dt) = DateTime::<Utc>::from_timestamp(epoch, 0) else {
return Ok(Value::Undefined);
};
// Always UTC, so use Z suffix.
Ok(Value::from(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()))
}
/// `dateTimeToEpoch(dateTime)` → Unix epoch seconds from ISO 8601 string.
fn fn_date_time_to_epoch(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(dt) = parse_datetime(s) else {
return Ok(Value::Undefined);
};
Ok(Value::from(dt.timestamp()))
}
/// `addDays(dateTime, numberOfDays)` → ISO 8601 datetime with days added.
///
/// Very common in real Azure Policy definitions (e.g., key expiry checks).
fn fn_add_days(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(base_str) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(days) = args.get(1).and_then(extract_i64) else {
return Ok(Value::Undefined);
};
let Some(base_dt) = parse_datetime(base_str) else {
return Ok(Value::Undefined);
};
let duration = Duration::days(days);
let result = base_dt
.checked_add_signed(duration)
.ok_or_else(|| anyhow::anyhow!("addDays: datetime overflow"))?;
Ok(Value::from(format_datetime(&result)))
}
// ── Helpers ───────────────────────────────────────────────────────────
fn extract_i64(v: &Value) -> Option<i64> {
match *v {
Value::Number(ref n) => n.as_i64(),
_ => None,
}
}
/// Deliberate truncating conversion from `f64` → `i64`.
#[expect(clippy::as_conversions)]
const fn f64_as_i64(x: f64) -> i64 {
x as i64
}
/// Segments produced by parsing a .NET custom datetime format string.
enum FmtSegment {
/// A chrono format string.
Chrono(String),
/// Fractional seconds, truncated to `n` digits (1..=7).
Frac(usize),
/// First character of AM/PM (the .NET `t` specifier).
AmPmShort,
/// Timezone offset hours, no leading zero (the .NET `z` specifier).
TzHoursNoPad,
/// Timezone offset hours, with leading zero (the .NET `zz` specifier).
TzHoursPad,
}
/// Result of trying to interpret a format string as a .NET standard specifier.
enum StandardFormat {
/// Expanded custom format string.
Expansion(String),
/// A standard specifier that requires UTC conversion before formatting.
UtcNormalized(String),
/// Not a single-letter standard specifier (treat as custom format).
NotStandard,
}
/// Format a datetime using a .NET-style date/time format string.
///
/// Handles both standard format strings (single character like `d`, `G`, `o`)
/// and custom format strings (multi-token patterns like `yyyy-MM-dd`).
///
/// # Compatibility note
///
/// This is an *approximation* of `System.DateTime.ToString()` targeting the
/// invariant culture only, which is what Azure Policy uses in practice. The
/// segment-based architecture (`FmtSegment` + `dotnet_to_segments`) covers
/// the specifiers exercised by real-world policy definitions, but
/// culture-sensitive corners (localised day/month names, era designators,
/// calendar systems, etc.) are deliberately omitted. Pulling in a full
/// ICU/globalisation stack would be disproportionate for this use case.
/// If a specific .NET format corner is needed later, it can be added
/// incrementally by extending the segment parser.
fn format_datetime_dotnet(dt: &DateTime<FixedOffset>, dotnet_fmt: &str) -> Result<String> {
// Check for standard format specifiers and determine the effective
// custom format and the datetime to format against.
let (effective_fmt_owned, format_dt);
let effective_fmt = match resolve_standard_format(dotnet_fmt)? {
StandardFormat::Expansion(s) => {
effective_fmt_owned = s;
&effective_fmt_owned
}
StandardFormat::UtcNormalized(s) => {
// Convert to UTC before formatting (e.g. the 'u' specifier).
format_dt = dt.with_timezone(&Utc).fixed_offset();
effective_fmt_owned = s;
let is_utc = true;
let segments = dotnet_to_segments(&effective_fmt_owned, is_utc);
return Ok(render_segments(&format_dt, &segments));
}
StandardFormat::NotStandard => dotnet_fmt,
};
let is_utc = dt.offset().local_minus_utc() == 0;
let segments = dotnet_to_segments(effective_fmt, is_utc);
Ok(render_segments(dt, &segments))
}
/// Render pre-parsed format segments against a datetime value.
fn render_segments(dt: &DateTime<FixedOffset>, segments: &[FmtSegment]) -> String {
let mut out = String::new();
for seg in segments {
match *seg {
FmtSegment::Chrono(ref fmt) => {
out.push_str(&dt.format(fmt).to_string());
}
FmtSegment::Frac(n) => {
// %f gives 9-digit nanoseconds; take the first n digits.
let nanos = dt.format("%f").to_string();
let truncated: String = nanos.chars().take(n).collect();
out.push_str(&truncated);
}
FmtSegment::AmPmShort => {
let full = dt.format("%p").to_string();
if let Some(c) = full.chars().next() {
out.push(c);
}
}
FmtSegment::TzHoursNoPad => {
let hours = dt.offset().local_minus_utc() / 3600;
if hours >= 0 {
out.push('+');
}
let _ = write!(out, "{hours}");
}
FmtSegment::TzHoursPad => {
let secs = dt.offset().local_minus_utc();
let hours = secs / 3600;
if secs >= 0 {
let _ = write!(out, "+{hours:02}");
} else {
let _ = write!(out, "-{:02}", hours.wrapping_neg());
}
}
}
}
out
}
/// Resolve a .NET standard date/time format specifier.
///
/// Returns the appropriate `StandardFormat` variant:
/// - `Expansion` for standard specifiers that can be expanded to custom tokens.
/// - `UtcNormalized` for specifiers that require UTC conversion first.
/// - `NotStandard` when the string is a multi-character custom format.
///
/// Single-letter strings that are *not* a recognised standard specifier are
/// also mapped to their equivalent custom token (via the .NET `%`-prefix
/// rule), so that e.g. `"U"` does not silently produce a literal `U`.
///
/// Reference: <https://learn.microsoft.com/dotnet/standard/base-types/standard-date-and-time-format-strings>
fn resolve_standard_format(fmt: &str) -> Result<StandardFormat> {
if fmt.len() != 1 {
return Ok(StandardFormat::NotStandard);
}
Ok(match fmt {
// Short date (invariant culture: MM/dd/yyyy)
"d" => StandardFormat::Expansion("MM/dd/yyyy".into()),
// Long date (invariant: dddd, dd MMMM yyyy)
"D" => StandardFormat::Expansion("dddd, dd MMMM yyyy".into()),
// Short time (invariant: HH:mm)
"t" => StandardFormat::Expansion("HH:mm".into()),
// Long time (invariant: HH:mm:ss)
"T" => StandardFormat::Expansion("HH:mm:ss".into()),
// General short time (short date + short time)
"g" => StandardFormat::Expansion("MM/dd/yyyy HH:mm".into()),
// General long time (short date + long time)
"G" => StandardFormat::Expansion("MM/dd/yyyy HH:mm:ss".into()),
// Month/day (invariant: MMMM dd)
"M" | "m" => StandardFormat::Expansion("MMMM dd".into()),
// Round-trip / ISO 8601 (o / O are identical)
"o" | "O" => StandardFormat::Expansion("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK".into()),
// RFC1123 (invariant: ddd, dd MMM yyyy HH:mm:ss 'GMT') — requires UTC conversion
"R" | "r" => StandardFormat::UtcNormalized("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'".into()),
// Sortable (ISO 8601 without offset)
"s" => StandardFormat::Expansion("yyyy'-'MM'-'dd'T'HH':'mm':'ss".into()),
// Universal sortable (UTC, trailing Z) — requires UTC conversion
"u" => StandardFormat::UtcNormalized("yyyy'-'MM'-'dd HH':'mm':'ss'Z'".into()),
// Full date/time (UTC) — requires UTC conversion
"U" => StandardFormat::UtcNormalized("dddd, dd MMMM yyyy HH:mm:ss".into()),
// Year/month (invariant: yyyy MMMM)
"Y" | "y" => StandardFormat::Expansion("yyyy MMMM".into()),
// Full date/short time
"f" => StandardFormat::Expansion("dddd, dd MMMM yyyy HH:mm".into()),
// Full date/long time
"F" => StandardFormat::Expansion("dddd, dd MMMM yyyy HH:mm:ss".into()),
// Not a standard specifier → error. In .NET, passing an
// unrecognised single-letter string to DateTime.ToString() throws
// FormatException rather than silently echoing the character.
_ => anyhow::bail!(
"dateTimeAdd: unrecognised standard format specifier '{}'",
fmt
),
})
}
/// Parse a .NET custom datetime format string into segments.
///
/// Consecutive chrono-compatible tokens are batched into a single `Chrono`
/// segment; tokens that need custom logic produce their own segment.
fn dotnet_to_segments(fmt: &str, is_utc: bool) -> Vec<FmtSegment> {
let mut segments: Vec<FmtSegment> = Vec::new();
let mut chrono_buf = String::new();
let chars: Vec<char> = fmt.chars().collect();
let len = chars.len();
let mut i: usize = 0;
macro_rules! flush {
() => {
if !chrono_buf.is_empty() {
segments.push(FmtSegment::Chrono(core::mem::take(&mut chrono_buf)));
}
};
}
while i < len {
let ch = chars.get(i).copied().unwrap_or('\0');
let remaining = len.saturating_sub(i);
match ch {
// Escaped literal
'\\' if remaining > 1 => {
i = i.wrapping_add(1);
let next = chars.get(i).copied().unwrap_or('\0');
chrono_buf.push(next);
i = i.wrapping_add(1);
}
// Quoted literal
'\'' => {
i = i.wrapping_add(1);
while i < len {
let c = chars.get(i).copied().unwrap_or('\0');
if c == '\'' {
i = i.wrapping_add(1);
break;
}
chrono_buf.push(c);
i = i.wrapping_add(1);
}
}
// Year
'y' if remaining >= 4 && matches_run(&chars, i, 'y', 4) => {
chrono_buf.push_str("%Y");
i = i.wrapping_add(4);
}
'y' if remaining >= 2 && matches_run(&chars, i, 'y', 2) => {
chrono_buf.push_str("%y");
i = i.wrapping_add(2);
}
// Month
'M' if remaining >= 4 && matches_run(&chars, i, 'M', 4) => {
chrono_buf.push_str("%B");
i = i.wrapping_add(4);
}
'M' if remaining >= 3 && matches_run(&chars, i, 'M', 3) => {
chrono_buf.push_str("%b");
i = i.wrapping_add(3);
}
'M' if remaining >= 2 && matches_run(&chars, i, 'M', 2) => {
chrono_buf.push_str("%m");
i = i.wrapping_add(2);
}
'M' => {
chrono_buf.push_str("%-m");
i = i.wrapping_add(1);
}
// Day
'd' if remaining >= 4 && matches_run(&chars, i, 'd', 4) => {
chrono_buf.push_str("%A");
i = i.wrapping_add(4);
}
'd' if remaining >= 3 && matches_run(&chars, i, 'd', 3) => {
chrono_buf.push_str("%a");
i = i.wrapping_add(3);
}
'd' if remaining >= 2 && matches_run(&chars, i, 'd', 2) => {
chrono_buf.push_str("%d");
i = i.wrapping_add(2);
}
'd' => {
chrono_buf.push_str("%-d");
i = i.wrapping_add(1);
}
// 24-hour
'H' if remaining >= 2 && matches_run(&chars, i, 'H', 2) => {
chrono_buf.push_str("%H");
i = i.wrapping_add(2);
}
'H' => {
chrono_buf.push_str("%-H");
i = i.wrapping_add(1);
}
// 12-hour
'h' if remaining >= 2 && matches_run(&chars, i, 'h', 2) => {
chrono_buf.push_str("%I");
i = i.wrapping_add(2);
}
'h' => {
chrono_buf.push_str("%-I");
i = i.wrapping_add(1);
}
// Minute
'm' if remaining >= 2 && matches_run(&chars, i, 'm', 2) => {
chrono_buf.push_str("%M");
i = i.wrapping_add(2);
}
'm' => {
chrono_buf.push_str("%-M");
i = i.wrapping_add(1);
}
// Second
's' if remaining >= 2 && matches_run(&chars, i, 's', 2) => {
chrono_buf.push_str("%S");
i = i.wrapping_add(2);
}
's' => {
chrono_buf.push_str("%-S");
i = i.wrapping_add(1);
}
// Fractions of second — consume the full run of 'f' chars
'f' => {
let mut count: usize = 0;
while i < len && chars.get(i).copied() == Some('f') {
count = count.wrapping_add(1);
i = i.wrapping_add(1);
}
flush!();
// Clamp to 9 (nanosecond precision from chrono).
segments.push(FmtSegment::Frac(count.min(9)));
}
// AM/PM
't' if remaining >= 2 && matches_run(&chars, i, 't', 2) => {
chrono_buf.push_str("%p");
i = i.wrapping_add(2);
}
't' => {
flush!();
segments.push(FmtSegment::AmPmShort);
i = i.wrapping_add(1);
}
// Timezone: K in .NET → offset or Z
'K' => {
if is_utc {
chrono_buf.push('Z');
} else {
chrono_buf.push_str("%:z");
}
i = i.wrapping_add(1);
}
// Timezone offset zzz → full offset +00:00
'z' if remaining >= 3 && matches_run(&chars, i, 'z', 3) => {
chrono_buf.push_str("%:z");
i = i.wrapping_add(3);
}
// zz → offset hours with leading zero
'z' if remaining >= 2 && matches_run(&chars, i, 'z', 2) => {
flush!();
segments.push(FmtSegment::TzHoursPad);
i = i.wrapping_add(2);
}
// z → offset hours without leading zero
'z' => {
flush!();
segments.push(FmtSegment::TzHoursNoPad);
i = i.wrapping_add(1);
}
// Literal characters (including T, :, -, etc.)
_ => {
chrono_buf.push(ch);
i = i.wrapping_add(1);
}
}
}
flush!();
segments
}
/// Check whether the slice starting at `start` contains at least `count`
/// consecutive occurrences of `ch`.
fn matches_run(chars: &[char], start: usize, ch: char, count: usize) -> bool {
(0..count).all(|offset| chars.get(start.wrapping_add(offset)).copied() == Some(ch))
}

View File

@@ -1,329 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template encoding builtins: base64, base64ToString, base64ToJson,
//! uri, uriComponent, uriComponentToString, dataUri, dataUriToString.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use super::helpers::as_str;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.base64", (fn_base64, 1));
m.insert("azure.policy.fn.base64_to_string", (fn_base64_to_string, 1));
m.insert("azure.policy.fn.base64_to_json", (fn_base64_to_json, 1));
m.insert("azure.policy.fn.uri", (fn_uri, 2));
m.insert("azure.policy.fn.uri_component", (fn_uri_component, 1));
m.insert(
"azure.policy.fn.uri_component_to_string",
(fn_uri_component_to_string, 1),
);
m.insert("azure.policy.fn.data_uri", (fn_data_uri, 1));
m.insert(
"azure.policy.fn.data_uri_to_string",
(fn_data_uri_to_string, 1),
);
}
// ── Base64 helpers (pure implementation, no external deps) ────────────
const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(input: &[u8]) -> String {
let cap = input.len().div_ceil(3).saturating_mul(4);
let mut result = String::with_capacity(cap);
for chunk in input.chunks(3) {
let b0 = u32::from(*chunk.first().unwrap_or(&0));
let b1 = u32::from(*chunk.get(1).unwrap_or(&0));
let b2 = u32::from(*chunk.get(2).unwrap_or(&0));
let triple = (b0 << 16) | (b1 << 8) | b2;
let idx0 = usize::try_from((triple >> 18) & 0x3F).unwrap_or(0);
let idx1 = usize::try_from((triple >> 12) & 0x3F).unwrap_or(0);
let idx2 = usize::try_from((triple >> 6) & 0x3F).unwrap_or(0);
let idx3 = usize::try_from(triple & 0x3F).unwrap_or(0);
result.push(char::from(*BASE64_CHARS.get(idx0).unwrap_or(&b'A')));
result.push(char::from(*BASE64_CHARS.get(idx1).unwrap_or(&b'A')));
if chunk.len() > 1 {
result.push(char::from(*BASE64_CHARS.get(idx2).unwrap_or(&b'A')));
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(char::from(*BASE64_CHARS.get(idx3).unwrap_or(&b'A')));
} else {
result.push('=');
}
}
result
}
const fn base64_decode_byte(c: u8) -> Option<u8> {
match c {
b'A'..=b'Z' => Some(c.wrapping_sub(b'A')),
b'a'..=b'z' => Some(c.wrapping_sub(b'a').wrapping_add(26)),
b'0'..=b'9' => Some(c.wrapping_sub(b'0').wrapping_add(52)),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
fn base64_decode(input: &str) -> Option<Vec<u8>> {
let input = input.trim();
if input.is_empty() {
return Some(Vec::new());
}
let bytes: Vec<u8> = input
.bytes()
.filter(|&b| b != b'\n' && b != b'\r')
.collect();
if !bytes.len().is_multiple_of(4) {
return None;
}
let mut result = Vec::with_capacity((bytes.len() / 4).saturating_mul(3));
for chunk in bytes.chunks(4) {
let [c0, c1, c2, c3] = <[u8; 4]>::try_from(chunk).ok()?;
let a = base64_decode_byte(c0)?;
let b = base64_decode_byte(c1)?;
let triple = u32::from(a) << 18
| u32::from(b) << 12
| if c2 != b'=' {
u32::from(base64_decode_byte(c2)?) << 6
} else {
0
}
| if c3 != b'=' {
u32::from(base64_decode_byte(c3)?)
} else {
0
};
result.push(u8::try_from((triple >> 16) & 0xFF).unwrap_or(0));
if c2 != b'=' {
result.push(u8::try_from((triple >> 8) & 0xFF).unwrap_or(0));
}
if c3 != b'=' {
result.push(u8::try_from(triple & 0xFF).unwrap_or(0));
}
}
Some(result)
}
/// `base64(inputString)` → base64-encoded string.
fn fn_base64(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
Ok(Value::from(base64_encode(s.as_bytes())))
}
/// `base64ToString(base64Value)` → decoded UTF-8 string.
fn fn_base64_to_string(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(decoded) = base64_decode(s) else {
return Ok(Value::Undefined);
};
String::from_utf8(decoded).map_or_else(|_| Ok(Value::Undefined), |text| Ok(Value::from(text)))
}
/// `base64ToJson(base64Value)` → parsed JSON value from base64-encoded string.
fn fn_base64_to_json(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(decoded) = base64_decode(s) else {
return Ok(Value::Undefined);
};
let Ok(text) = String::from_utf8(decoded) else {
return Ok(Value::Undefined);
};
Value::from_json_str(&text).map_or_else(|_| Ok(Value::Undefined), Ok)
}
// ── URI helpers (pure implementation) ─────────────────────────────────
const fn is_unreserved(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.' || b == b'~'
}
fn percent_encode(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for &b in s.as_bytes() {
if is_unreserved(b) {
result.push(char::from(b));
} else {
// b >> 4 is in 0..=15 and b & 0x0F is in 0..=15, so from_digit
// always returns Some for radix 16.
result.push('%');
result.push(
core::char::from_digit(u32::from(b >> 4), 16)
.unwrap_or('0')
.to_ascii_uppercase(),
);
result.push(
core::char::from_digit(u32::from(b & 0x0F), 16)
.unwrap_or('0')
.to_ascii_uppercase(),
);
}
}
result
}
fn percent_decode(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut result = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if *bytes.get(i)? == b'%' {
// Require exactly two hex digits after '%'; reject incomplete escapes.
let hi = char::from(*bytes.get(i.checked_add(1)?)?).to_digit(16)?;
let lo = char::from(*bytes.get(i.checked_add(2)?)?).to_digit(16)?;
result.push(u8::try_from(hi.checked_mul(16)?.checked_add(lo)?).ok()?);
i = i.checked_add(3)?;
} else {
result.push(*bytes.get(i)?);
i = i.checked_add(1)?;
}
}
String::from_utf8(result).ok()
}
/// `uri(baseUri, relativeUri)` → combined URI.
fn fn_uri(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
if args.len() != 2 {
return Ok(Value::Undefined);
}
let (Some(base), Some(relative)) =
(args.first().and_then(as_str), args.get(1).and_then(as_str))
else {
return Ok(Value::Undefined);
};
// Simple URI combination following Azure semantics.
let combined = if relative.starts_with("http://") || relative.starts_with("https://") {
// Relative is an absolute URL — use it directly.
relative.to_string()
} else if base.ends_with('/') {
alloc::format!("{}{}", base, relative.trim_start_matches('/'))
} else {
// Find the end of the authority (scheme://host). The path starts
// after the third '/' (e.g. https://example.com/path → slash at
// position after "com"). If there is no path component at all
// (e.g. "https://example.com"), just append.
let scheme_end = base.find("://").map(|p| p.wrapping_add(3)).unwrap_or(0);
let path_slash = base.get(scheme_end..).and_then(|rest| rest.find('/'));
path_slash.map_or_else(
|| {
// Authority-only base (no path) — append with slash.
alloc::format!("{}/{}", base, relative.trim_start_matches('/'))
},
|offset| {
// There is a path — replace the last segment.
let abs_pos = scheme_end.wrapping_add(offset);
let last_slash = base
.get(abs_pos..)
.and_then(|p| p.rfind('/'))
.map(|o| abs_pos.wrapping_add(o));
let cut = last_slash.unwrap_or(abs_pos);
alloc::format!(
"{}/{}",
base.get(..cut).unwrap_or(base),
relative.trim_start_matches('/')
)
},
)
};
Ok(Value::from(combined))
}
/// `uriComponent(stringToEncode)` → percent-encoded string.
fn fn_uri_component(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
Ok(Value::from(percent_encode(s)))
}
/// `uriComponentToString(uriEncodedString)` → decoded string.
fn fn_uri_component_to_string(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
percent_decode(s).map_or_else(|| Ok(Value::Undefined), |decoded| Ok(Value::from(decoded)))
}
/// `dataUri(stringToConvert)` → data URI (text/plain;charset=utf8).
fn fn_data_uri(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let encoded = base64_encode(s.as_bytes());
Ok(Value::from(alloc::format!(
"data:text/plain;charset=utf8;base64,{}",
encoded
)))
}
/// `dataUriToString(dataUriToConvert)` → decoded string from data URI.
fn fn_data_uri_to_string(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
// Expected format: data:<mediatype>;base64,<data>
let Some(rest) = s.strip_prefix("data:") else {
return Ok(Value::Undefined);
};
// Find the base64 data after the last comma.
let Some(comma_pos) = rest.rfind(',') else {
return Ok(Value::Undefined);
};
let b64_data = rest.get(comma_pos.saturating_add(1)..).unwrap_or("");
let Some(decoded) = base64_decode(b64_data) else {
return Ok(Value::Undefined);
};
String::from_utf8(decoded).map_or_else(|_| Ok(Value::Undefined), |text| Ok(Value::from(text)))
}

View File

@@ -1,197 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template miscellaneous function builtins for Azure Policy expressions.
//!
//! Implements: json, join, items, indexFromEnd, tryGet, tryIndexFromEnd.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use super::helpers::as_str;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.json", (fn_json, 1));
m.insert("azure.policy.fn.join", (fn_join, 2));
m.insert("azure.policy.fn.items", (fn_items, 1));
m.insert("azure.policy.fn.index_from_end", (fn_index_from_end, 2));
m.insert("azure.policy.fn.try_get", (fn_try_get, 2));
m.insert(
"azure.policy.fn.try_index_from_end",
(fn_try_index_from_end, 2),
);
// TODO: implement guid() and uniqueString() — need a SHA-2 based
// deterministic hash (FNV-1a could be used as a lighter alternative
// since these functions don't serve a security purpose).
}
// ── json ──────────────────────────────────────────────────────────────
/// `json(arg)` → parses a JSON string into a typed value.
///
/// `json('null')` returns null, `json('{"a":1}')` returns an object, etc.
fn fn_json(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
Value::from_json_str(s).map_err(|e| anyhow::anyhow!("json(): {}", e))
}
// ── join ──────────────────────────────────────────────────────────────
/// `join(inputArray, delimiter)` → joins array elements with delimiter.
fn fn_join(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [arr_val, delim_val] = args
else {
return Ok(Value::Undefined);
};
let Value::Array(ref arr) = *arr_val else {
return Ok(Value::Undefined);
};
let Some(delim) = as_str(delim_val) else {
return Ok(Value::Undefined);
};
let parts: Vec<String> = arr
.iter()
.map(|v| match *v {
Value::String(ref s) => s.to_string(),
Value::Number(ref n) => n.format_decimal(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => v.to_string(),
})
.collect();
Ok(Value::from(parts.join(delim)))
}
// ── items ─────────────────────────────────────────────────────────────
/// `items(object)` → array of `{"key": k, "value": v}` pairs.
fn fn_items(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
let Value::Object(ref obj) = *first else {
return Ok(Value::Undefined);
};
let mut result = Vec::with_capacity(obj.len());
for (k, v) in obj.as_ref() {
let mut entry = BTreeMap::<Value, Value>::new();
entry.insert(Value::from("key"), k.clone());
entry.insert(Value::from("value"), v.clone());
result.push(Value::Object(Rc::new(entry)));
}
Ok(Value::Array(Rc::new(result)))
}
// ── indexFromEnd ──────────────────────────────────────────────────────
/// `indexFromEnd(sourceArray, reverseIndex)` → element at 1-based reverse index.
///
/// `indexFromEnd([a,b,c,d], 2)` returns `c` (2nd from end).
fn fn_index_from_end(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [arr_val, idx_val] = args
else {
return Ok(Value::Undefined);
};
let Value::Array(ref arr) = *arr_val else {
return Ok(Value::Undefined);
};
let Some(rev_idx) = extract_usize(idx_val) else {
return Ok(Value::Undefined);
};
if rev_idx == 0 || rev_idx > arr.len() {
anyhow::bail!("indexFromEnd: reverse index {} out of bounds", rev_idx);
}
let pos = arr
.len()
.checked_sub(rev_idx)
.ok_or_else(|| anyhow::anyhow!("indexFromEnd: arithmetic overflow"))?;
arr.get(pos)
.cloned()
.ok_or_else(|| anyhow::anyhow!("indexFromEnd: index out of bounds"))
}
// ── tryGet ────────────────────────────────────────────────────────────
/// `tryGet(itemToTest, keyOrIndex)` → value at key/index, or null if missing.
fn fn_try_get(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [item, key_or_idx] = args
else {
return Ok(Value::Undefined);
};
match *item {
Value::Object(ref obj) => {
// Property lookup by string key
Ok(obj.get(key_or_idx).cloned().unwrap_or(Value::Null))
}
Value::Array(ref arr) => {
// Index lookup
let Some(idx) = extract_usize(key_or_idx) else {
return Ok(Value::Null);
};
Ok(arr.get(idx).cloned().unwrap_or(Value::Null))
}
_ => Ok(Value::Null),
}
}
// ── tryIndexFromEnd ───────────────────────────────────────────────────
/// `tryIndexFromEnd(sourceArray, reverseIndex)` → element or null if out of bounds.
fn fn_try_index_from_end(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [arr_val, idx_val] = args
else {
return Ok(Value::Undefined);
};
let Value::Array(ref arr) = *arr_val else {
return Ok(Value::Null);
};
let Some(rev_idx) = extract_usize(idx_val) else {
return Ok(Value::Null);
};
if rev_idx == 0 || rev_idx > arr.len() {
return Ok(Value::Null);
}
let pos = arr.len().saturating_sub(rev_idx);
Ok(arr.get(pos).cloned().unwrap_or(Value::Null))
}
// ── Helpers ───────────────────────────────────────────────────────────
fn extract_usize(v: &Value) -> Option<usize> {
match *v {
Value::Number(ref n) => n
.as_i64()
.and_then(|x| usize::try_from(x).ok())
.or_else(|| n.as_f64().and_then(|x| usize::try_from(f64_to_i64(x)).ok())),
_ => None,
}
}
#[expect(clippy::as_conversions)]
const fn f64_to_i64(x: f64) -> i64 {
x as i64
}

View File

@@ -1,155 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template numeric function builtins for Azure Policy expressions.
//!
//! Implements: min, max, float.
//!
//! Note: `sub`, `mul`, `div`, `mod` are compiled directly to native RVM
//! instructions (`Sub`, `Mul`, `Div`, `Mod`) and do not need builtins.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use anyhow::Result;
use super::helpers::try_coerce_to_number;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.min", (fn_min, super::MAX_VARIADIC_ARGS));
m.insert("azure.policy.fn.max", (fn_max, super::MAX_VARIADIC_ARGS));
m.insert("azure.policy.fn.float", (fn_float, 1));
m.insert("azure.policy.fn.int_div", (fn_int_div, 2));
m.insert("azure.policy.fn.int_mod", (fn_int_mod, 2));
}
/// `min(arg1, arg2, ...)` or `min(intArray)` → smallest integer/number.
///
/// Accepts either:
/// - Multiple integer arguments: `min(1, 2, 3)` → `1`
/// - A single array argument: `min([1, 2, 3])` → `1`
fn fn_min(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let values = if args.len() == 1 {
args.first().map_or(args, |first| match *first {
Value::Array(ref arr) => arr.as_ref().as_slice(),
_ => args,
})
} else {
args
};
if values.is_empty() {
return Ok(Value::Undefined);
}
let mut result: Option<&Value> = None;
for v in values {
match *v {
Value::Number(_) => {
result = Some(match result {
Some(current) if v >= current => current,
_ => v,
});
}
_ => return Ok(Value::Undefined),
}
}
Ok(result.cloned().unwrap_or(Value::Undefined))
}
/// `max(arg1, arg2, ...)` or `max(intArray)` → largest integer/number.
///
/// Same overloading as `min`.
fn fn_max(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let values = if args.len() == 1 {
args.first().map_or(args, |first| match *first {
Value::Array(ref arr) => arr.as_ref().as_slice(),
_ => args,
})
} else {
args
};
if values.is_empty() {
return Ok(Value::Undefined);
}
let mut result: Option<&Value> = None;
for v in values {
match *v {
Value::Number(_) => {
result = Some(match result {
Some(current) if v <= current => current,
_ => v,
});
}
_ => return Ok(Value::Undefined),
}
}
Ok(result.cloned().unwrap_or(Value::Undefined))
}
/// `float(value)` → floating-point number.
fn fn_float(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Number(ref n) => Ok(n.as_f64().map_or(Value::Undefined, Value::from)),
Value::String(ref s) => Ok(try_coerce_to_number(s)
.and_then(|n| n.as_f64())
.map_or(Value::Undefined, Value::from)),
_ => Ok(Value::Undefined),
}
}
/// `div(operand1, operand2)` → integer division (truncating).
///
/// ARM template `div()` performs integer division, unlike the RVM `Div`
/// instruction which may produce floats for non-evenly-divisible operands.
fn fn_int_div(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [a, b] = args
else {
return Ok(Value::Undefined);
};
let (a, b) = (extract_i64(a), extract_i64(b));
match (a, b) {
(Some(a), Some(b)) => a
.checked_div(b)
.map_or(Ok(Value::Undefined), |r| Ok(Value::from(r))),
_ => Ok(Value::Undefined),
}
}
/// `mod(operand1, operand2)` → integer modulo.
fn fn_int_mod(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [a, b] = args
else {
return Ok(Value::Undefined);
};
let (a, b) = (extract_i64(a), extract_i64(b));
match (a, b) {
(Some(a), Some(b)) => a
.checked_rem(b)
.map_or(Ok(Value::Undefined), |r| Ok(Value::from(r))),
_ => Ok(Value::Undefined),
}
}
fn extract_i64(v: &Value) -> Option<i64> {
match *v {
Value::Number(ref n) => n.as_i64().or_else(|| n.as_f64().map(f64_to_i64)),
_ => None,
}
}
#[expect(clippy::as_conversions)]
const fn f64_to_i64(x: f64) -> i64 {
x as i64
}

View File

@@ -1,474 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! ARM template string function builtins for Azure Policy expressions.
//!
//! Implements: indexOf, lastIndexOf, trim, format.
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::languages::azure_policy::strings::case_fold;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use super::helpers::as_str;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.index_of", (fn_index_of, 2));
m.insert("azure.policy.fn.last_index_of", (fn_last_index_of, 2));
m.insert("azure.policy.fn.trim", (fn_trim, 1));
m.insert(
"azure.policy.fn.format",
(fn_format, super::MAX_VARIADIC_ARGS),
);
}
/// `indexOf(stringToSearch, stringToFind)` → zero-based character index, or -1 if not found.
///
/// Azure documents this as case-insensitive. Uses full Unicode case folding
/// via ICU4X (`case_fold::fold`) and returns a *character* index (not byte).
fn fn_index_of(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Undefined);
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Undefined);
};
Ok(Value::from(case_insensitive_index_of(haystack, needle)))
}
/// `lastIndexOf(stringToSearch, stringToFind)` → zero-based character index, or -1.
///
/// Azure documents this as case-insensitive. Uses full Unicode case folding
/// and returns a *character* index.
fn fn_last_index_of(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Undefined);
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Undefined);
};
Ok(Value::from(case_insensitive_last_index_of(
haystack, needle,
)))
}
/// Case-insensitive first-occurrence search returning a *UTF-16 code-unit* index.
///
/// Azure/ARM string functions are .NET-based — indices are UTF-16 code units,
/// not Rust `char` (Unicode scalar) positions. We track the UTF-16 offset
/// in `fold_with_char_map` so that surrogate-pair characters are counted
/// correctly.
///
/// Case-folds both strings using ICU4X and searches in the folded domain.
/// The haystack is folded in a single pass that simultaneously builds a
/// byte-to-UTF-16-offset mapping, avoiding a redundant second fold.
fn case_insensitive_index_of(haystack: &str, needle: &str) -> i64 {
let folded_needle = case_fold::fold(needle);
if folded_needle.is_empty() {
return 0;
}
let (folded_hay, byte_to_utf16) = fold_with_char_map(haystack);
if folded_needle.len() > folded_hay.len() {
return -1;
}
folded_hay
.find(&*folded_needle)
.and_then(|byte_pos| byte_to_utf16.get(byte_pos).copied())
.and_then(|ci| i64::try_from(ci).ok())
.unwrap_or(-1)
}
/// Case-insensitive last-occurrence search returning a *UTF-16 code-unit* index.
fn case_insensitive_last_index_of(haystack: &str, needle: &str) -> i64 {
let folded_needle = case_fold::fold(needle);
if folded_needle.is_empty() {
return i64::try_from(haystack.encode_utf16().count()).unwrap_or(-1);
}
let (folded_hay, byte_to_utf16) = fold_with_char_map(haystack);
if folded_needle.len() > folded_hay.len() {
return -1;
}
folded_hay
.rfind(&*folded_needle)
.and_then(|byte_pos| byte_to_utf16.get(byte_pos).copied())
.and_then(|ci| i64::try_from(ci).ok())
.unwrap_or(-1)
}
/// Case-fold a string one character at a time, returning both the folded
/// string and a byte-to-UTF-16-offset map in a single pass.
///
/// Each source character contributes `ch.len_utf16()` to the running
/// UTF-16 offset, so non-BMP codepoints (surrogate pairs) are counted
/// as two units — matching .NET `String.IndexOf` semantics.
///
/// # Performance note
///
/// This function allocates a folded copy of the haystack and a parallel
/// `Vec<usize>` mapping every folded byte back to a source UTF-16 offset.
/// The allocation is inherent to Unicode case folding — you need the folded
/// string to search in it. For the string sizes typical in Azure Policy
/// templates (field names, resource type strings) this is negligible. If
/// profiling ever shows this as a hot path on very large inputs, a streaming
/// fold-and-match approach could replace it, but that is speculative
/// optimisation at this point.
fn fold_with_char_map(s: &str) -> (String, Vec<usize>) {
let mut folded = String::with_capacity(s.len());
let mut map = Vec::with_capacity(s.len());
let mut utf16_offset: usize = 0;
for (byte_idx, ch) in s.char_indices() {
let ch_len = ch.len_utf8();
let end = byte_idx.wrapping_add(ch_len);
let ch_slice = s.get(byte_idx..end).unwrap_or("");
let folded_ch = case_fold::fold(ch_slice);
folded.push_str(&folded_ch);
for _ in 0..folded_ch.len() {
map.push(utf16_offset);
}
utf16_offset = utf16_offset.wrapping_add(ch.len_utf16());
}
(folded, map)
}
/// `trim(stringToTrim)` → string with leading/trailing whitespace removed.
fn fn_trim(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
let Some(s) = as_str(arg) else {
return Ok(Value::Undefined);
};
Ok(Value::from(s.trim().to_string()))
}
/// `format(formatString, arg0, arg1, ...)` → formatted string.
///
/// ARM template format matches `System.String.Format` conventions:
/// - `{index[,alignment][:formatString]}` placeholders
/// - `{{` and `}}` are escaped literal braces
/// - Alignment: positive = right-padded, negative = left-padded
/// - Numeric format strings: N/n (number with thousands), D/d (decimal),
/// X/x (hex), F/f (fixed-point), etc.
fn fn_format(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
if args.is_empty() {
return Ok(Value::Undefined);
}
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
let Some(template) = as_str(first) else {
return Ok(Value::Undefined);
};
let format_args: Vec<String> = args
.get(1..)
.unwrap_or_default()
.iter()
.map(|v| match *v {
Value::String(ref s) => s.to_string(),
Value::Number(ref n) => n.format_decimal(),
Value::Bool(true) => "True".into(),
Value::Bool(false) => "False".into(),
Value::Null => String::new(),
_ => v.to_string(),
})
.collect();
let format_args_values = args.get(1..).unwrap_or_default();
let mut result = String::new();
let chars: Vec<char> = template.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
let ch = chars.get(i).copied().unwrap_or('\0');
match ch {
'{' => {
// Check for escaped brace {{
if chars.get(i.wrapping_add(1)).copied() == Some('{') {
result.push('{');
i = i.wrapping_add(2);
continue;
}
// Parse placeholder: {index[,alignment][:formatString]}
i = i.wrapping_add(1); // skip '{'
let mut index_str = String::new();
while i < len && chars.get(i).copied().unwrap_or('\0').is_ascii_digit() {
index_str.push(chars.get(i).copied().unwrap_or('\0'));
i = i.wrapping_add(1);
}
if index_str.is_empty() {
anyhow::bail!(
"format: invalid placeholder at position {}; expected '{{index}}'",
i.wrapping_sub(1)
);
}
let idx: usize = index_str.parse().map_err(|_| {
anyhow::anyhow!(
"format: invalid placeholder index '{}' at position {}; expected a non-negative integer within range",
index_str,
i.wrapping_sub(index_str.len()).wrapping_sub(1)
)
})?;
// Optional alignment
let alignment: i32 = if chars.get(i).copied() == Some(',') {
i = i.wrapping_add(1); // skip ','
let mut align_str = String::new();
while i < len {
let c = chars.get(i).copied().unwrap_or('\0');
if c == ':' || c == '}' {
break;
}
align_str.push(c);
i = i.wrapping_add(1);
}
let trimmed = align_str.trim();
if trimmed.is_empty() {
anyhow::bail!(
"format: empty alignment value in placeholder at position {}",
i.wrapping_sub(align_str.len()).wrapping_sub(1)
);
}
trimmed.parse().map_err(|_| {
anyhow::anyhow!(
"format: invalid alignment '{}' in placeholder; expected an integer",
trimmed
)
})?
} else {
0
};
// Optional format specifier
let mut fmt_spec = String::new();
if chars.get(i).copied() == Some(':') {
i = i.wrapping_add(1); // skip ':'
while i < len && chars.get(i).copied().unwrap_or('\0') != '}' {
fmt_spec.push(chars.get(i).copied().unwrap_or('\0'));
i = i.wrapping_add(1);
}
}
// Require closing '}'
if chars.get(i).copied() == Some('}') {
i = i.wrapping_add(1);
} else {
anyhow::bail!(
"format: unmatched opening brace '{{{{' at position {}; \
expected closing '}}}}'.",
i.wrapping_sub(index_str.len()).wrapping_sub(1)
);
}
// Format the argument — error if the index is out of range,
// matching System.String.Format semantics.
let Some(raw) = format_args.get(idx).cloned() else {
anyhow::bail!(
"format: placeholder {{{idx}}} references argument index {idx}, \
but only {} argument(s) were supplied",
format_args.len()
);
};
let formatted = if fmt_spec.is_empty() {
raw
} else {
apply_format_spec(&raw, &fmt_spec, format_args_values.get(idx))?
};
// Apply alignment
apply_alignment(&mut result, &formatted, alignment)?;
}
'}' => {
// Escaped }} → literal '}'
if chars.get(i.wrapping_add(1)).copied() == Some('}') {
result.push('}');
i = i.wrapping_add(2);
} else {
anyhow::bail!("format: unmatched closing brace '}}' at position {i}");
}
}
_ => {
result.push(ch);
i = i.wrapping_add(1);
}
}
}
Ok(Value::from(result))
}
/// Apply a .NET-style format specifier to a value string.
fn apply_format_spec(raw: &str, spec: &str, value: Option<&Value>) -> Result<String> {
let spec_char = spec.chars().next().unwrap_or('G');
let precision: Option<usize> = spec.get(1..).and_then(|s| s.parse().ok());
// Try to get numeric value for numeric formatting
let int_val = value.and_then(|v| match *v {
Value::Number(ref n) => n.as_i64(),
_ => None,
});
let float_val = value.and_then(|v| match *v {
Value::Number(ref n) => n.as_f64(),
_ => None,
});
Ok(match spec_char {
// Fixed-point
'F' | 'f' => {
let prec = precision.unwrap_or(2);
float_val.map_or_else(|| raw.to_string(), |f| alloc::format!("{f:.prec$}"))
}
// Number with thousands separator
'N' | 'n' => {
let prec = precision.unwrap_or(2);
float_val.map_or_else(|| raw.to_string(), |f| format_with_thousands(f, prec))
}
// Decimal (integer)
'D' | 'd' => {
let width = precision.unwrap_or(0);
int_val.map_or_else(
|| raw.to_string(),
|n| {
if n < 0 {
alloc::format!("-{:0>width$}", n.unsigned_abs())
} else {
alloc::format!("{n:0>width$}")
}
},
)
}
// Hexadecimal
'X' => {
let width = precision.unwrap_or(0);
int_val.map_or_else(
|| raw.to_string(),
|n| alloc::format!("{:0>width$}", alloc::format!("{n:X}")),
)
}
'x' => {
let width = precision.unwrap_or(0);
int_val.map_or_else(
|| raw.to_string(),
|n| alloc::format!("{:0>width$}", alloc::format!("{n:x}")),
)
}
// Percent
'P' | 'p' => {
let prec = precision.unwrap_or(2);
float_val.map_or_else(
|| raw.to_string(),
|f| {
let pct = f * 100.0;
alloc::format!("{pct:.prec$} %")
},
)
}
// Unknown specifier: error for numeric values (.NET throws FormatException),
// pass through for non-numeric.
_ => {
if int_val.is_some() || float_val.is_some() {
anyhow::bail!("format: invalid numeric format specifier '{spec_char}'");
}
raw.to_string()
}
})
}
/// Format a number with thousands separators and fixed decimal places.
fn format_with_thousands(value: f64, precision: usize) -> String {
let formatted = alloc::format!("{value:.precision$}");
let (int_part, dec_part) = formatted.split_once('.').unwrap_or((&formatted, ""));
let negative = int_part.starts_with('-');
let digits = if negative {
int_part.get(1..).unwrap_or("")
} else {
int_part
};
let mut with_commas = String::new();
for (i, ch) in digits.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
with_commas.push(',');
}
with_commas.push(ch);
}
let with_commas: String = with_commas.chars().rev().collect();
let mut result = String::new();
if negative {
result.push('-');
}
result.push_str(&with_commas);
if precision > 0 {
result.push('.');
result.push_str(dec_part);
}
result
}
/// Maximum alignment width to prevent excessive memory allocation from
/// user-controlled format strings (e.g. `{0,1000000000}`).
const MAX_ALIGNMENT_WIDTH: usize = 10_000;
/// Apply alignment (padding) to a formatted value.
fn apply_alignment(result: &mut String, formatted: &str, alignment: i32) -> Result<()> {
if alignment == 0 {
result.push_str(formatted);
} else {
let width = usize::try_from(alignment.unsigned_abs()).unwrap_or(usize::MAX);
if width > MAX_ALIGNMENT_WIDTH {
anyhow::bail!(
"format: alignment width {width} exceeds maximum allowed ({MAX_ALIGNMENT_WIDTH})"
);
}
let char_len = formatted.chars().count();
if char_len >= width {
result.push_str(formatted);
} else {
let padding = width.saturating_sub(char_len);
if alignment > 0 {
// Right-align: pad on left
for _ in 0..padding {
result.push(' ');
}
result.push_str(formatted);
} else {
// Left-align: pad on right
result.push_str(formatted);
for _ in 0..padding {
result.push(' ');
}
}
}
}
Ok(())
}

View File

@@ -52,33 +52,11 @@ fn make_delimiters_unix_style(s: &str, delimiters: &[char]) -> Result<String> {
}
fn make_glob(pattern: &str, span: &Span) -> Result<GlobMatcher> {
#[cfg(feature = "cache")]
{
{
let mut cache = crate::cache::GLOB_CACHE.lock();
if let Some(matcher) = cache.get(pattern) {
return Ok(matcher.clone());
}
}
let matcher = GlobBuilder::new(pattern)
.literal_separator(true)
.build()
.or_else(|_| bail!(span.error("invalid glob")))?
.compile_matcher();
{
let mut cache = crate::cache::GLOB_CACHE.lock();
cache.put(alloc::string::String::from(pattern), matcher.clone());
}
Ok(matcher)
}
#[cfg(not(feature = "cache"))]
{
Ok(GlobBuilder::new(pattern)
.literal_separator(true)
.build()
.or_else(|_| bail!(span.error("invalid glob")))?
.compile_matcher())
}
Ok(GlobBuilder::new(pattern)
.literal_separator(true)
.build()
.or_else(|_| bail!(span.error("invalid glob")))?
.compile_matcher())
}
fn glob_match(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {

View File

@@ -16,8 +16,6 @@
mod aggregates;
mod arrays;
#[cfg(feature = "azure_policy")]
pub mod azure_policy;
mod bitwise;
pub mod comparison;
mod conversions;
@@ -106,8 +104,6 @@ lazy_static! {
#[cfg(feature = "semver")]
semver::register(&mut m);
//rego::register(&mut m);
#[cfg(feature = "azure_policy")]
azure_policy::register(&mut m);
#[cfg(feature = "opa-runtime")]
opa::register(&mut m);
tracing::register(&mut m);

View File

@@ -19,7 +19,7 @@ use crate::*;
use anyhow::{bail, Result};
#[cfg(feature = "std")]
use rand::RngExt;
use rand::Rng;
use vstd::prelude::*;

View File

@@ -12,39 +12,6 @@ use crate::*;
use anyhow::{bail, Result};
use regex::Regex;
// ---------------------------------------------------------------------------
// Compiled-regex cache (feature = "cache")
//
// When enabled, compiled Regex objects are stored in a bounded LRU cache
// protected by a Mutex (parking_lot when std, spin when no_std).
// The capacity is configurable at runtime
// via regorus::cache::configure().
// ---------------------------------------------------------------------------
/// Compile a regex pattern, using the cache when the `cache` feature
/// is enabled and falling back to direct compilation otherwise.
fn get_or_compile_regex(pattern: &str) -> core::result::Result<Regex, regex::Error> {
#[cfg(feature = "cache")]
{
{
let mut cache = crate::cache::REGEX_CACHE.lock();
if let Some(re) = cache.get(pattern) {
return Ok(re.clone());
}
}
let re = Regex::new(pattern)?;
{
let mut cache = crate::cache::REGEX_CACHE.lock();
cache.put(alloc::string::String::from(pattern), re.clone());
Ok(re)
}
}
#[cfg(not(feature = "cache"))]
{
Regex::new(pattern)
}
}
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert(
"regex.find_all_string_submatch_n",
@@ -72,8 +39,8 @@ fn find_all_string_submatch_n(
let value = ensure_string(name, &params[1], &args[1])?;
let n = ensure_numeric(name, &params[2], &args[2])?;
let re = get_or_compile_regex(&pattern)
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
let pattern =
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
if !n.is_integer() {
bail!(params[2].span().error("n must be an integer"));
@@ -86,7 +53,8 @@ fn find_all_string_submatch_n(
};
Ok(Value::from_array(
re.captures_iter(&value)
pattern
.captures_iter(&value)
.map(|capture| {
let groups = capture
.iter()
@@ -118,8 +86,8 @@ fn find_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
let value = ensure_string(name, &params[1], &args[1])?;
let n = ensure_numeric(name, &params[2], &args[2])?;
let re = get_or_compile_regex(&pattern)
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
let pattern =
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
if !n.is_integer() {
bail!(params[2].span().error("n must be an integer"));
@@ -132,7 +100,8 @@ fn find_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
};
Ok(Value::from_array(
re.find_iter(&value)
pattern
.find_iter(&value)
.map(|m| {
let value = Value::String(m.as_str().into());
// Guard match accumulation while pushing each substring.
@@ -147,11 +116,8 @@ fn find_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
fn is_valid(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let name = "regex.is_valid";
ensure_args_count(span, name, params, args, 1)?;
Ok(
ensure_string(name, &params[0], &args[0]).map_or(Value::Bool(false), |p| {
Value::Bool(get_or_compile_regex(&p).is_ok())
}),
)
Ok(ensure_string(name, &params[0], &args[0])
.map_or(Value::Bool(false), |p| Value::Bool(Regex::new(&p).is_ok())))
}
pub fn regex_match(
@@ -165,9 +131,9 @@ pub fn regex_match(
let pattern = ensure_string(name, &params[0], &args[0])?;
let value = ensure_string(name, &params[1], &args[1])?;
let re = get_or_compile_regex(&pattern)
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
Ok(Value::Bool(re.is_match(&value)))
let pattern =
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
Ok(Value::Bool(pattern.is_match(&value)))
}
fn regex_replace(
@@ -183,13 +149,15 @@ fn regex_replace(
let pattern = ensure_string(name, &params[1], &args[1])?;
let value = ensure_string(name, &params[2], &args[2])?;
let re = match get_or_compile_regex(&pattern) {
let pattern = match Regex::new(&pattern) {
Ok(p) => p,
// TODO: This behavior is due to OPA test not raising error. Should we raise error?
_ => return Ok(Value::Undefined),
};
Ok(Value::String(re.replace_all(&s, value.as_ref()).into()))
Ok(Value::String(
pattern.replace_all(&s, value.as_ref()).into(),
))
}
fn regex_split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
@@ -198,10 +166,11 @@ fn regex_split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool)
let pattern = ensure_string(name, &params[0], &args[0])?;
let value = ensure_string(name, &params[1], &args[1])?;
let re = get_or_compile_regex(&pattern)
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
let pattern =
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
Ok(Value::from_array(
re.split(&value)
pattern
.split(&value)
.map(|s| {
let value = Value::String(s.into());
// Guard output accumulation as each split segment is emitted.
@@ -242,13 +211,13 @@ fn regex_template_match(
}
// Fetch pattern, excluding delimiters.
let re = get_or_compile_regex(&template[start + delimiter_start.len()..end])
let pattern = Regex::new(&template[start + delimiter_start.len()..end])
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
// Skip preceding literal in value.
value = &value[start..];
let m = match re.find(value) {
let m = match pattern.find(value) {
Some(m) if m.start() == 0 => m,
_ => return Ok(Value::Bool(false)),
};

View File

@@ -1,171 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Compiled-pattern caches for Rego builtins.
//!
//! When the `cache` feature is enabled, compiled [`regex::Regex`] and
//! [`globset::GlobMatcher`] objects are held in bounded LRU caches so that
//! repeated evaluations of the same pattern avoid recompilation.
//!
//! # Examples
//!
//! ```ignore
//! use regorus::cache;
//!
//! // Configure cache capacities (0 = disabled).
//! cache::configure(cache::Config {
//! regex: 256,
//! glob: 128,
//! });
//!
//! // Flush all cached patterns.
//! cache::clear();
//! ```
#[cfg(any(feature = "regex", feature = "glob"))]
use core::num::NonZeroUsize;
#[cfg(any(feature = "regex", feature = "glob"))]
use lazy_static::lazy_static;
#[cfg(all(feature = "std", any(feature = "regex", feature = "glob")))]
use parking_lot::Mutex;
#[cfg(all(not(feature = "std"), any(feature = "regex", feature = "glob")))]
use spin::Mutex;
#[cfg(any(feature = "regex", feature = "glob"))]
use alloc::string::String;
/// Configuration for builtin pattern caches.
///
/// Each field controls the maximum number of compiled patterns held in the
/// corresponding LRU cache. A value of `0` disables that cache entirely
/// (every lookup recompiles). Values exceeding [`Config::MAX_CAPACITY`] are
/// clamped silently.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// Maximum compiled regex patterns (default 256).
pub regex: usize,
/// Maximum compiled glob matchers (default 128).
pub glob: usize,
}
impl Config {
/// Hard upper bound for any single cache capacity (2^16 = 65 536).
pub const MAX_CAPACITY: usize = 1 << 16;
}
impl Default for Config {
fn default() -> Self {
Self {
regex: 256,
glob: 128,
}
}
}
// ---------------------------------------------------------------------------
// Internal generic LRU wrapper
// ---------------------------------------------------------------------------
#[cfg(any(feature = "regex", feature = "glob"))]
pub(crate) struct LruCache<V> {
inner: Option<lru::LruCache<String, V>>,
}
#[cfg(any(feature = "regex", feature = "glob"))]
impl<V> LruCache<V> {
pub(crate) fn new(capacity: usize) -> Self {
Self {
inner: NonZeroUsize::new(capacity).map(lru::LruCache::new),
}
}
/// Look up a key, returning a reference if present. Promotes to most-recent.
pub(crate) fn get(&mut self, key: &str) -> Option<&V> {
self.inner.as_mut()?.get(key)
}
/// Insert a key-value pair. Evicts the least-recently-used entry if full.
pub(crate) fn put(&mut self, key: String, value: V) {
if let Some(cache) = self.inner.as_mut() {
cache.put(key, value);
}
}
/// Remove all entries.
pub(crate) fn clear(&mut self) {
if let Some(cache) = self.inner.as_mut() {
cache.clear();
}
}
/// Resize the cache. If new capacity is 0, disables the cache.
pub(crate) fn resize(&mut self, capacity: usize) {
match NonZeroUsize::new(capacity) {
Some(cap) => match self.inner.as_mut() {
Some(cache) => cache.resize(cap),
None => self.inner = Some(lru::LruCache::new(cap)),
},
None => {
self.inner = None;
}
}
}
/// Number of entries currently cached.
#[allow(dead_code)]
pub(crate) fn len(&self) -> usize {
self.inner.as_ref().map_or(0, lru::LruCache::len)
}
}
// ---------------------------------------------------------------------------
// Global regex cache
// ---------------------------------------------------------------------------
#[cfg(feature = "regex")]
lazy_static! {
pub(crate) static ref REGEX_CACHE: Mutex<LruCache<regex::Regex>> =
Mutex::new(LruCache::new(Config::default().regex));
}
// ---------------------------------------------------------------------------
// Global glob cache
// ---------------------------------------------------------------------------
#[cfg(feature = "glob")]
lazy_static! {
pub(crate) static ref GLOB_CACHE: Mutex<LruCache<globset::GlobMatcher>> =
Mutex::new(LruCache::new(Config::default().glob));
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Apply a new cache configuration.
///
/// Resizes each cache to the specified capacity. Existing entries are
/// preserved (subject to LRU eviction if the new capacity is smaller).
/// Values exceeding [`Config::MAX_CAPACITY`] are clamped.
pub fn configure(config: Config) {
let regex = config.regex.min(Config::MAX_CAPACITY);
let glob = config.glob.min(Config::MAX_CAPACITY);
#[cfg(feature = "regex")]
REGEX_CACHE.lock().resize(regex);
#[cfg(feature = "glob")]
GLOB_CACHE.lock().resize(glob);
// Suppress unused-variable warnings when neither regex nor glob is enabled.
let _ = (regex, glob);
}
/// Remove all entries from every pattern cache.
pub fn clear() {
#[cfg(feature = "regex")]
REGEX_CACHE.lock().clear();
#[cfg(feature = "glob")]
GLOB_CACHE.lock().clear();
}

View File

@@ -9,7 +9,6 @@ use crate::lexer::*;
use crate::parser::*;
use crate::scheduler::*;
use crate::utils::gather_functions;
use crate::utils::limits::PolicyLengthConfig;
use crate::utils::limits::{self, fallback_execution_timer_config, ExecutionTimerConfig};
use crate::value::*;
use crate::*;
@@ -27,7 +26,6 @@ pub struct Engine {
prepared: bool,
rego_v1: bool,
execution_timer_config: Option<ExecutionTimerConfig>,
policy_length_config: PolicyLengthConfig,
}
#[cfg(feature = "azure_policy")]
@@ -85,7 +83,6 @@ impl Engine {
prepared: false,
rego_v1: true,
execution_timer_config: None,
policy_length_config: PolicyLengthConfig::default(),
};
engine.apply_effective_execution_timer_config();
engine
@@ -147,31 +144,6 @@ impl Engine {
self.interpreter.set_execution_timer_config(Some(config));
}
/// Set the policy length limits used when loading policies.
///
/// Controls maximum file size, line count, and column width for policy files.
/// Engines start with the default limits defined by [`PolicyLengthConfig::default`].
///
/// # Examples
///
/// ```
/// use core::num::{NonZeroU32, NonZeroUsize};
/// use regorus::utils::limits::PolicyLengthConfig;
/// use regorus::Engine;
///
/// let mut engine = Engine::new();
/// let config = PolicyLengthConfig {
/// max_col: NonZeroU32::new(2048).unwrap(),
/// max_file_bytes: NonZeroUsize::new(2_097_152).unwrap(),
/// max_lines: NonZeroUsize::new(40_000).unwrap(),
/// };
///
/// engine.set_policy_length_config(config);
/// ```
pub const fn set_policy_length_config(&mut self, config: PolicyLengthConfig) {
self.policy_length_config = config;
}
/// Clear the engine-specific execution timer configuration, falling back to the global value.
///
/// # Examples
@@ -199,20 +171,6 @@ impl Engine {
self.apply_effective_execution_timer_config();
}
/// Clear the policy length configuration, reverting to the defaults.
///
/// # Examples
///
/// ```
/// use regorus::Engine;
///
/// let mut engine = Engine::new();
/// engine.clear_policy_length_config();
/// ```
pub fn clear_policy_length_config(&mut self) {
self.policy_length_config = PolicyLengthConfig::default();
}
/// Add a policy.
///
/// The policy file will be parsed and converted to AST representation.
@@ -240,12 +198,7 @@ impl Engine {
/// ```
///
pub fn add_policy(&mut self, path: String, rego: String) -> Result<String> {
let source = Source::from_contents_with_limits(
path,
rego,
self.policy_length_config.max_file_bytes,
self.policy_length_config.max_lines,
)?;
let source = Source::from_contents(path, rego)?;
let mut parser = self.make_parser(&source)?;
let module = Ref::new(parser.parse()?);
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
@@ -279,11 +232,7 @@ impl Engine {
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn add_policy_from_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<String> {
let source = Source::from_file_with_limits(
path,
self.policy_length_config.max_file_bytes,
self.policy_length_config.max_lines,
)?;
let source = Source::from_file(path)?;
let mut parser = self.make_parser(&source)?;
let module = Ref::new(parser.parse()?);
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
@@ -969,24 +918,15 @@ impl Engine {
fn make_query(&mut self, query: String) -> Result<(NodeRef<Module>, NodeRef<Query>, Schedule)> {
let mut query_module = {
let source = Source::from_contents_with_limits(
let source = Source::from_contents(
"<query_module.rego>".to_owned(),
"package __internal_query_module".to_owned(),
self.policy_length_config.max_file_bytes,
self.policy_length_config.max_lines,
)?;
let mut parser = Parser::new(&source)?;
parser.set_max_col(self.policy_length_config.max_col);
parser.parse()?
Parser::new(&source)?.parse()?
};
// Parse the query.
let query_source = Source::from_contents_with_limits(
"<query.rego>".to_string(),
query,
self.policy_length_config.max_file_bytes,
self.policy_length_config.max_lines,
)?;
let query_source = Source::from_contents("<query.rego>".to_string(), query)?;
let mut parser = self.make_parser(&query_source)?;
let query_node = parser.parse_user_query()?;
query_module.num_expressions = parser.num_expressions();
@@ -1566,7 +1506,6 @@ impl Engine {
fn make_parser<'a>(&self, source: &'a Source) -> Result<Parser<'a>> {
let mut parser = Parser::new(source)?;
parser.set_max_col(self.policy_length_config.max_col);
if self.rego_v1 {
parser.enable_rego_v1()?;
}
@@ -1585,7 +1524,6 @@ impl Engine {
rego_v1: true, // Value doesn't matter since this is used only for policy parsing
prepared: true,
execution_timer_config: None,
policy_length_config: PolicyLengthConfig::default(), // Compiled policies are already parsed, so these length limits are not used
};
engine.apply_effective_execution_timer_config();
engine

View File

@@ -289,7 +289,7 @@ impl Interpreter {
}
fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
if !self.execution_timer.accumulate(work_units) {
if self.execution_timer.limit().is_none() {
return Ok(());
}
@@ -297,7 +297,7 @@ impl Interpreter {
return Ok(());
};
self.execution_timer.check_now(now)?;
self.execution_timer.tick(work_units, now)?;
Ok(())
}
@@ -402,13 +402,13 @@ impl Interpreter {
self.reset_execution_timer_state();
}
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
#[cfg(feature = "allocator-memory-limits")]
fn memory_check(&mut self) -> Result<()> {
let _ = self; // quiet clippy::unused_self; retained for symmetry with VM path
crate::utils::limits::check_memory_limit_if_needed().map_err(|err| anyhow!(err))
}
#[cfg(any(miri, not(feature = "allocator-memory-limits")))]
#[cfg(not(feature = "allocator-memory-limits"))]
const fn memory_check(&mut self) -> Result<()> {
let _ = self; // quiet clippy::unused_self; retained for symmetry with VM path
Ok(())

View File

@@ -1,114 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Key casing restoration from alias metadata.
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use crate::Value;
use super::super::obj_map::{make_array, make_value, new_map, obj_insert, val_str, ROOT_FIELDS};
use super::super::types::ResolvedEntry;
fn insert_default_casing(map: &mut BTreeMap<String, String>) {
for &field in ROOT_FIELDS {
map.insert(field.to_ascii_lowercase(), field.to_string());
}
// Canonical casing for standard nested root-field object members that are
// not described by alias metadata but still need round-trip restoration.
for canonical in [
"principalId",
"tenantId",
"userAssignedIdentities",
"promotionCode",
"createdBy",
"createdByType",
"createdAt",
"lastModifiedBy",
"lastModifiedByType",
"lastModifiedAt",
] {
map.entry(canonical.to_ascii_lowercase())
.or_insert_with(|| canonical.to_string());
}
}
/// Build the default casing map used when alias metadata is unavailable.
pub fn default_casing_map() -> BTreeMap<String, String> {
let mut map = BTreeMap::new();
insert_default_casing(&mut map);
map
}
/// Build a mapping from lowercase key → original-cased key from alias entries.
pub fn build_casing_map(entries: &BTreeMap<String, ResolvedEntry>) -> BTreeMap<String, String> {
let mut map = BTreeMap::new();
insert_default_casing(&mut map);
for entry in entries.values() {
for segment in entry.short_name.split('.') {
let clean = segment.replace("[*]", "");
if !clean.is_empty() {
map.entry(clean.to_ascii_lowercase())
.or_insert_with(|| clean.to_string());
}
}
for segment in entry.default_path.split('.') {
let clean = segment.replace("[*]", "");
if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") {
map.entry(clean.to_ascii_lowercase())
.or_insert_with(|| clean.to_string());
}
}
// Also include segments from all version-specific ARM paths so
// casing can be restored correctly for versioned aliases.
for (_ver, path) in &entry.versioned_paths {
for segment in path.split('.') {
let clean = segment.replace("[*]", "");
if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") {
map.entry(clean.to_ascii_lowercase())
.or_insert_with(|| clean.to_string());
}
}
}
}
map
}
/// Restore the original casing of a key using the casing map.
pub fn restore_casing(key: &str, casing_map: &BTreeMap<String, String>) -> String {
casing_map
.get(&key.to_ascii_lowercase())
.cloned()
.unwrap_or_else(|| key.to_string())
}
/// Recursively restore key casing in a JSON value.
pub fn denormalize_value(value: &Value, casing_map: &BTreeMap<String, String>) -> Value {
match value {
Value::Object(obj) => {
let mut result = new_map();
for (k, v) in obj.iter() {
if let Some(key_s) = val_str(k) {
let restored_key = restore_casing(key_s, casing_map);
obj_insert(&mut result, &restored_key, denormalize_value(v, casing_map));
}
}
make_value(result)
}
Value::Array(arr) => {
let items: Vec<Value> = arr
.iter()
.map(|v| denormalize_value(v, casing_map))
.collect();
make_array(items)
}
_ => value.clone(),
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Small helper functions used by the denormalizer.
use crate::Rc;
use super::super::obj_map::ObjMap;
/// Find a key in an ObjMap using case-insensitive comparison.
pub fn find_key_ci(obj: &ObjMap, key: &str) -> Option<Rc<str>> {
obj.keys().find(|k| k.eq_ignore_ascii_case(key)).cloned()
}

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