mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Compare commits
44 Commits
regorus-v0
...
verus2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5112ccf492 | ||
|
|
8f740e2f6f | ||
|
|
687be2850b | ||
|
|
95bffcb5f9 | ||
|
|
db8a9abf13 | ||
|
|
421ee6af9b | ||
|
|
64f71dee34 | ||
|
|
648ba40126 | ||
|
|
126cc12eb5 | ||
|
|
1a8fc08773 | ||
|
|
c164917d63 | ||
|
|
6a6cc659b7 | ||
|
|
a86cf1119f | ||
|
|
989ca6df2e | ||
|
|
d36f952133 | ||
|
|
35fb5d5953 | ||
|
|
296b34171a | ||
|
|
f9d54cd436 | ||
|
|
5b60daabd9 | ||
|
|
f69974dc1b | ||
|
|
942dd47163 | ||
|
|
ac701b4933 | ||
|
|
86088d2049 | ||
|
|
83891d7782 | ||
|
|
898643129e | ||
|
|
50c0215fdb | ||
|
|
ee3dff9a3d | ||
|
|
b8e15f46f3 | ||
|
|
37144968c8 | ||
|
|
7ee503ccdc | ||
|
|
006e819d52 | ||
|
|
b6f11c5602 | ||
|
|
72033e77da | ||
|
|
be34063dba | ||
|
|
04bf417c06 | ||
|
|
bc23cd08ac | ||
|
|
1c607dc1d3 | ||
|
|
47cc27ff49 | ||
|
|
8814eda0ae | ||
|
|
b4a69a13ba | ||
|
|
e83a47497a | ||
|
|
241c1d445b | ||
|
|
4054d1b6b6 | ||
|
|
8f7ca44bdf |
92
.github/dependabot.yml
vendored
92
.github/dependabot.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
@@ -5,7 +7,95 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# All Rust/Cargo directories are grouped into a single entry so that
|
||||
# when a dependency is updated, Dependabot bumps it across the root
|
||||
# workspace AND every binding, preventing version skew.
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/" # Location of package manifests
|
||||
directories:
|
||||
- "/"
|
||||
- "/bindings/ffi"
|
||||
- "/bindings/java"
|
||||
- "/bindings/python"
|
||||
- "/bindings/ruby"
|
||||
- "/bindings/wasm"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
# Bundle all Cargo dependency updates into a single PR. Without this,
|
||||
# dependabot creates a separate PR per directory for the same dependency,
|
||||
# and each individual PR fails to build due to version skew.
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
# Ignore vendored mimalloc crates; updates are managed manually.
|
||||
ignore:
|
||||
- dependency-name: "regorus-mimalloc"
|
||||
- dependency-name: "regorus-mimalloc-sys"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/bindings/go"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
per-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/bindings/java"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
per-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "nuget"
|
||||
directory: "/bindings/csharp"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
per-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/bindings/python"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
per-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/bindings/ruby"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "build(deps)"
|
||||
groups:
|
||||
per-dependency:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci(deps)"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
22
.github/workflows/codeql.yml
vendored
22
.github/workflows/codeql.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: "CodeQL Security Analysis"
|
||||
|
||||
on:
|
||||
@@ -60,14 +62,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# Setup language-specific dependencies BEFORE CodeQL init for proper tracing setup
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
|
||||
@@ -84,26 +86,26 @@ jobs:
|
||||
|
||||
- name: Setup Python
|
||||
if: matrix.language == 'python'
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Setup Java
|
||||
if: matrix.language == 'java-kotlin'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: 'corretto'
|
||||
java-version: '8'
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Setup .NET
|
||||
if: matrix.language == 'csharp'
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ./bindings/csharp/global.json
|
||||
|
||||
@@ -113,12 +115,12 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: matrix.language == 'javascript-typescript'
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -139,7 +141,7 @@ jobs:
|
||||
|
||||
- name: Setup Ruby
|
||||
if: matrix.language == 'rust' && contains(matrix.working-directory, 'ruby')
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0
|
||||
with:
|
||||
ruby-version: '3.4.2'
|
||||
bundler-cache: true
|
||||
@@ -186,6 +188,6 @@ jobs:
|
||||
run: cargo xtask build-wasm --release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
129
.github/workflows/dependabot-refresh-cargo-lockfiles.yml
vendored
Normal file
129
.github/workflows/dependabot-refresh-cargo-lockfiles.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: dependabot/refresh-cargo-lockfiles
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: ["main"]
|
||||
|
||||
concurrency:
|
||||
group: dependabot-refresh-cargo-lockfiles-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
refresh-cargo-lockfiles:
|
||||
permissions:
|
||||
contents: write
|
||||
if: >-
|
||||
github.event.pull_request.user.login == 'dependabot[bot]' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
run: |
|
||||
rustup toolchain install 1.92.0 --profile minimal
|
||||
rustup override set 1.92.0
|
||||
cargo --version
|
||||
rustc --version
|
||||
|
||||
- name: Refresh affected Cargo lockfiles
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
mapfile -t changed_files < <(git diff --name-only "$base_sha" "$head_sha" -- ':(glob)**/Cargo.toml' ':(glob)**/Cargo.lock')
|
||||
|
||||
if [ "${#changed_files[@]}" -eq 0 ]; then
|
||||
echo "No Cargo manifest or lockfile changes detected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
declare -A manifests=()
|
||||
for path in "${changed_files[@]}"; do
|
||||
case "$path" in
|
||||
bindings/ffi/*)
|
||||
manifests["bindings/ffi/Cargo.toml"]=1
|
||||
;;
|
||||
bindings/java/*)
|
||||
manifests["bindings/java/Cargo.toml"]=1
|
||||
;;
|
||||
bindings/python/*)
|
||||
manifests["bindings/python/Cargo.toml"]=1
|
||||
;;
|
||||
bindings/ruby/*)
|
||||
manifests["bindings/ruby/Cargo.toml"]=1
|
||||
;;
|
||||
bindings/wasm/*)
|
||||
manifests["bindings/wasm/Cargo.toml"]=1
|
||||
;;
|
||||
*)
|
||||
manifests["Cargo.toml"]=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for manifest in "${!manifests[@]}"; do
|
||||
echo "Refreshing lockfile for $manifest"
|
||||
cargo metadata \
|
||||
--config 'build.rustc="rustc"' \
|
||||
--config 'build.rustc-wrapper=""' \
|
||||
--config 'build.rustc-workspace-wrapper=""' \
|
||||
--format-version 1 \
|
||||
--all-features \
|
||||
--manifest-path "$manifest" > /dev/null
|
||||
done
|
||||
|
||||
if [[ -n "${manifests[Cargo.toml]+x}" ]]; then
|
||||
echo "Refreshing lockfile for tests/ensure_no_std/Cargo.toml (thumbv7m-none-eabi)"
|
||||
cargo metadata \
|
||||
--config 'build.rustc="rustc"' \
|
||||
--config 'build.rustc-wrapper=""' \
|
||||
--config 'build.rustc-workspace-wrapper=""' \
|
||||
--format-version 1 \
|
||||
--manifest-path tests/ensure_no_std/Cargo.toml \
|
||||
--filter-platform thumbv7m-none-eabi > /dev/null
|
||||
fi
|
||||
|
||||
- name: Commit lockfile refresh
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mapfile -t lockfiles < <(git ls-files -m -o --exclude-standard -- ':(glob)**/Cargo.lock')
|
||||
|
||||
for lockfile in "${lockfiles[@]}"; do
|
||||
git add "$lockfile"
|
||||
done
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No Cargo lockfile changes required."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
auth_header=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
|
||||
trap 'git config --unset-all http.https://github.com/.extraheader' EXIT
|
||||
git config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "build(deps): refresh Cargo lockfiles"
|
||||
git push origin HEAD:${{ github.event.pull_request.head.ref }}
|
||||
66
.github/workflows/dependency-audit.yml
vendored
Normal file
66
.github/workflows/dependency-audit.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Dependency Audits
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cargo-audit:
|
||||
name: Cargo Audit (${{ matrix.lockfile }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
lockfile:
|
||||
- Cargo.lock
|
||||
- bindings/ffi/Cargo.lock
|
||||
- bindings/java/Cargo.lock
|
||||
- bindings/python/Cargo.lock
|
||||
- bindings/ruby/Cargo.lock
|
||||
- bindings/wasm/Cargo.lock
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run cargo audit
|
||||
uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
lockfile: ${{ matrix.lockfile }}
|
||||
|
||||
cargo-deny:
|
||||
name: Cargo Deny (${{ matrix.manifest }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
manifest:
|
||||
- Cargo.toml
|
||||
- bindings/ffi/Cargo.toml
|
||||
- bindings/java/Cargo.toml
|
||||
- bindings/python/Cargo.toml
|
||||
- bindings/ruby/Cargo.toml
|
||||
- bindings/ruby/ext/regorusrb/Cargo.toml
|
||||
- bindings/wasm/Cargo.toml
|
||||
- tests/ensure_no_std/Cargo.toml
|
||||
- xtask/Cargo.toml
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
|
||||
- name: Run cargo deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check
|
||||
command-arguments: advisories bans
|
||||
manifest-path: ${{ matrix.manifest }}
|
||||
82
.github/workflows/feature-matrix.yml
vendored
Normal file
82
.github/workflows/feature-matrix.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
# Thorough weekly test of non-default feature combinations.
|
||||
# Catches regressions from dependency updates and feature-gating issues
|
||||
# that the fast PR CI checks (cargo check only) would miss at runtime.
|
||||
name: tests/feature-matrix
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Run at 3:42 AM UTC every Saturday.
|
||||
- cron: "42 3 * * 6"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
feature-matrix:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Bare minimum: validates that the core interpreter works
|
||||
# without any builtins or optional subsystems.
|
||||
- name: minimal (std + arc)
|
||||
features: std,arc
|
||||
|
||||
# Common library usage pattern (issue #595): consumer enables
|
||||
# std + arc + rvm and relies on indexmap/std propagation.
|
||||
- name: library (std + arc + rvm)
|
||||
features: std,arc,rvm
|
||||
|
||||
# New default after removing mimalloc from full-opa.
|
||||
# Ensures all builtins compile without the allocator.
|
||||
- name: full-opa (no mimalloc)
|
||||
features: std,arc,full-opa
|
||||
|
||||
# Binding-style usage: full-opa with the vendored allocator.
|
||||
# Mirrors how ffi/java/python/ruby bindings are built.
|
||||
- name: full-opa + allocator
|
||||
features: std,arc,full-opa,allocator-memory-limits
|
||||
|
||||
# Selective builtins without full-opa: validates that popular
|
||||
# features can be cherry-picked independently.
|
||||
- name: cherry-picked builtins
|
||||
features: std,arc,rvm,regex,time,semver,cache
|
||||
|
||||
# Observability features only: coverage + cache without the
|
||||
# heavier builtins (regex, time, etc.).
|
||||
- name: observability
|
||||
features: std,arc,rvm,coverage,cache
|
||||
|
||||
# Azure Policy adds jsonschema + dashmap; test it compiles
|
||||
# and runs on top of full-opa.
|
||||
- name: azure-policy
|
||||
features: std,arc,full-opa,azure_policy
|
||||
|
||||
# Azure RBAC adds regex + time + net on top of full-opa.
|
||||
- name: azure-rbac
|
||||
features: std,arc,full-opa,azure-rbac
|
||||
|
||||
# no_std with the OPA-compatible feature set: exercises the
|
||||
# spin_no_std codepath and absence of std-only dependencies.
|
||||
- name: no_std
|
||||
features: arc,opa-no-std
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus-features
|
||||
- name: Fetch dependencies
|
||||
run: cargo fetch --locked
|
||||
- name: Build
|
||||
run: cargo build --no-default-features --features "${{ matrix.features }}" --frozen
|
||||
- name: Test
|
||||
run: cargo test --no-default-features --features "${{ matrix.features }}" --frozen
|
||||
29
.github/workflows/miri.yml
vendored
Normal file
29
.github/workflows/miri.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: miri
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Run at 6:30 AM UTC every Wednesday
|
||||
- cron: "30 6 * * 3"
|
||||
|
||||
jobs:
|
||||
miri-test:
|
||||
name: miri (nightly)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MIRIFLAGS: "-Zmiri-disable-isolation"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: miri rust-src
|
||||
- name: Set up Miri
|
||||
run: cargo miri setup
|
||||
- name: Run Miri tests
|
||||
run: cargo miri test -p regorus
|
||||
- name: Run Miri ACI tests
|
||||
run: cargo miri test -p regorus --test aci
|
||||
- name: Run Miri kata tests
|
||||
run: cargo miri test -p regorus --test kata
|
||||
6
.github/workflows/pr-extensions.yml
vendored
6
.github/workflows/pr-extensions.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: tests/release-extensions
|
||||
|
||||
on:
|
||||
@@ -18,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
6
.github/workflows/pr.yml
vendored
6
.github/workflows/pr.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: tests/release
|
||||
|
||||
on:
|
||||
@@ -18,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
21
.github/workflows/publish-java.yml
vendored
21
.github/workflows/publish-java.yml
vendored
@@ -1,6 +1,9 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: publish-java
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -32,10 +35,10 @@ jobs:
|
||||
os: windows-latest
|
||||
extension: dll
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "corretto"
|
||||
@@ -43,7 +46,7 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- if: ${{ matrix.build_cmd == 'zigbuild' }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- if: ${{ matrix.build_cmd == 'zigbuild' }}
|
||||
@@ -53,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: native-libraries-${{ matrix.target }}
|
||||
path: native/
|
||||
@@ -63,24 +66,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "corretto"
|
||||
server-id: ossrh
|
||||
server-username: MAVEN_USERNAME
|
||||
server-password: MAVEN_PASSWORD
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: native-libraries-*
|
||||
merge-multiple: true
|
||||
path: ./bindings/java/native/
|
||||
- run: mvn package
|
||||
working-directory: ./bindings/java
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: built-jars
|
||||
path: ./bindings/java/target/regorus-java-*.jar
|
||||
|
||||
30
.github/workflows/publish-python.yml
vendored
30
.github/workflows/publish-python.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
# This file is autogenerated by maturin v1.4.0
|
||||
# To update, run
|
||||
#
|
||||
@@ -18,8 +20,8 @@ jobs:
|
||||
matrix:
|
||||
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
@@ -32,14 +34,14 @@ jobs:
|
||||
working-directory: bindings/python
|
||||
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
|
||||
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: wheels-linux-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -50,8 +52,8 @@ jobs:
|
||||
matrix:
|
||||
target: [x64, x86]
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
architecture: ${{ matrix.target }}
|
||||
@@ -65,13 +67,13 @@ jobs:
|
||||
working-directory: bindings/python
|
||||
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
|
||||
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --manifest-path bindings/python/Cargo.toml --frozen --strip
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: wheels-windows-${{ matrix.target }}
|
||||
path: dist
|
||||
@@ -82,8 +84,8 @@ jobs:
|
||||
matrix:
|
||||
target: [x86_64, aarch64, universal2-apple-darwin]
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
@@ -96,13 +98,13 @@ jobs:
|
||||
working-directory: bindings/python
|
||||
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
|
||||
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: wheels-macos-${{ matrix.host.target }}
|
||||
path: dist
|
||||
@@ -114,13 +116,13 @@ jobs:
|
||||
# if: "startsWith(github.ref, 'refs/tags/')"
|
||||
needs: [linux, windows, macos]
|
||||
steps:
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: wheels-*
|
||||
merge-multiple: true
|
||||
path: wheels
|
||||
- name: Publish to PyPI
|
||||
uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0
|
||||
uses: PyO3/maturin-action@04ac600d27cdf7a9a280dadf7147097c42b757ad # v1.43.0
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
with:
|
||||
|
||||
9
.github/workflows/publish-wasm.yml
vendored
9
.github/workflows/publish-wasm.yml
vendored
@@ -1,10 +1,13 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: publish-wasm
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish-wasm:
|
||||
@@ -12,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
9
.github/workflows/release-plz.yml
vendored
9
.github/workflows/release-plz.yml
vendored
@@ -1,10 +1,13 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: Release-plz
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-plz:
|
||||
@@ -14,13 +17,13 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Run release-plz
|
||||
uses: MarcoIeni/release-plz-action@8724d33cd97b8295051102e2e19ca592962238f5 #v0.5.108
|
||||
uses: MarcoIeni/release-plz-action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5.128
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
10
.github/workflows/rust-clippy.yml
vendored
10
.github/workflows/rust-clippy.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
@@ -30,12 +32,12 @@ jobs:
|
||||
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
|
||||
@@ -47,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
|
||||
uses: github/codeql-action/upload-sarif@c298edae2d512d807fe4bdc57c0ac5a036f61501 # v3.29.11
|
||||
if: ${{ hashFiles('rust-clippy-results.sarif') != '' }}
|
||||
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v3.29.11
|
||||
with:
|
||||
sarif_file: rust-clippy-results.sarif
|
||||
wait-for-processing: true
|
||||
|
||||
6
.github/workflows/test-c-cpp.yml
vendored
6
.github/workflows/test-c-cpp.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/c-cpp
|
||||
|
||||
on:
|
||||
@@ -14,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
26
.github/workflows/test-csharp.yml
vendored
26
.github/workflows/test-csharp.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/csharp
|
||||
|
||||
on:
|
||||
@@ -37,14 +39,14 @@ jobs:
|
||||
**/release/libregorus_ffi.dylib
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
with:
|
||||
targets: ${{ matrix.runtime.target }}
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
@@ -57,7 +59,7 @@ jobs:
|
||||
run: cargo xtask build-ffi --release --target ${{ matrix.runtime.target }}
|
||||
|
||||
- name: Upload regorus ffi shared library
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: regorus-ffi-artifacts-${{ matrix.runtime.target }}
|
||||
# Note: The full path of each artifact relative to . is preserved.
|
||||
@@ -71,18 +73,18 @@ jobs:
|
||||
needs: build-ffi
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ./bindings/csharp/global.json
|
||||
|
||||
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
|
||||
@@ -90,7 +92,7 @@ jobs:
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Download regorus ffi shared libraries
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: regorus-ffi-artifacts-*
|
||||
merge-multiple: true
|
||||
@@ -103,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: regorus-nuget
|
||||
path: |
|
||||
@@ -129,20 +131,20 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
|
||||
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
global-json-file: ./bindings/csharp/global.json
|
||||
|
||||
- run: echo '${{ steps.stepid.outputs.dotnet-version }}'
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
|
||||
@@ -150,7 +152,7 @@ jobs:
|
||||
run: cargo fetch --locked
|
||||
|
||||
- name: Download regorus nuget
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: regorus-nuget
|
||||
path: ./bindings/csharp/Regorus/bin/Release
|
||||
|
||||
6
.github/workflows/test-ffi.yml
vendored
6
.github/workflows/test-ffi.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/ffi
|
||||
|
||||
on:
|
||||
@@ -14,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
8
.github/workflows/test-go.yml
vendored
8
.github/workflows/test-go.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/go
|
||||
|
||||
on:
|
||||
@@ -14,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
- name: Fetch FFI crate dependencies
|
||||
run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml
|
||||
|
||||
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
architecture: x64
|
||||
|
||||
|
||||
8
.github/workflows/test-java.yml
vendored
8
.github/workflows/test-java.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/java
|
||||
|
||||
on:
|
||||
@@ -14,17 +16,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 8
|
||||
distribution: "corretto"
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
6
.github/workflows/test-musl.yml
vendored
6
.github/workflows/test-musl.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: musl
|
||||
|
||||
on:
|
||||
@@ -18,12 +20,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
with:
|
||||
targets: x86_64-unknown-linux-musl
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
6
.github/workflows/test-no-std.yml
vendored
6
.github/workflows/test-no-std.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/no-std
|
||||
|
||||
on:
|
||||
@@ -18,12 +20,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
with:
|
||||
targets: thumbv7m-none-eabi
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
16
.github/workflows/test-python.yml
vendored
16
.github/workflows/test-python.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/python
|
||||
|
||||
on:
|
||||
@@ -21,14 +23,14 @@ jobs:
|
||||
runs-on: ${{ matrix.host.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
with:
|
||||
targets: ${{ matrix.host.target }}
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
@@ -37,7 +39,7 @@ jobs:
|
||||
- name: Fetch Python crate dependencies
|
||||
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml --target ${{ matrix.host.target }}
|
||||
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.10"
|
||||
architecture: x64
|
||||
@@ -49,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: regorus-wheel-${{ matrix.host.name }}
|
||||
path: bindings/python/dist/regorus-*.whl
|
||||
@@ -63,12 +65,12 @@ jobs:
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
@@ -77,7 +79,7 @@ jobs:
|
||||
- name: Fetch Python crate dependencies
|
||||
run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml
|
||||
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: x64
|
||||
|
||||
8
.github/workflows/test-ruby.yml
vendored
8
.github/workflows/test-ruby.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/ruby
|
||||
|
||||
on:
|
||||
@@ -12,12 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Ruby and Rust
|
||||
uses: oxidize-rb/actions/setup-ruby-and-rust@7ca44a16e287e5ff7dd72ab53f4bd41cbf34a571 #v1.26
|
||||
uses: oxidize-rb/actions/setup-ruby-and-rust@e5f9a49a7812a078584072f6e3f657ad247c8771 # v1.26
|
||||
with:
|
||||
bundler: 2.6.5
|
||||
rubygems: 3.6.5
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
working-directory: "bindings/ruby"
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
|
||||
|
||||
8
.github/workflows/test-wasm.yml
vendored
8
.github/workflows/test-wasm.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: bindings/wasm
|
||||
|
||||
on:
|
||||
@@ -14,14 +16,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
@@ -31,7 +33,7 @@ jobs:
|
||||
run: cargo fetch --locked --manifest-path bindings/wasm/Cargo.toml
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
6
.github/workflows/tests-debug.yml
vendored
6
.github/workflows/tests-debug.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
#
|
||||
name: tests/debug
|
||||
|
||||
on:
|
||||
@@ -18,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Rust toolchain
|
||||
uses: ./.github/actions/toolchains/rust
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
shared-key: ${{ runner.os }}-regorus
|
||||
- name: Fetch dependencies
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -25,6 +25,12 @@ bindings/ffi/regorus.ffi.hpp
|
||||
|
||||
bindings/*/target
|
||||
|
||||
# Temporary commit message files
|
||||
.commit-msg.txt
|
||||
|
||||
# Local planning docs
|
||||
docs/plans/
|
||||
|
||||
# C# build folders
|
||||
**bin
|
||||
**obj
|
||||
@@ -48,3 +54,6 @@ bindings/ruby/bin/
|
||||
bindings/java/.classpath
|
||||
bindings/java/.project
|
||||
bindings/java/.settings/
|
||||
|
||||
# Emacs temporary files
|
||||
*~
|
||||
|
||||
@@ -6,6 +6,13 @@ 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
|
||||
|
||||
794
Cargo.lock
generated
794
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@@ -14,6 +14,11 @@ license = "MIT AND Apache-2.0 AND BSD-3-Clause"
|
||||
repository = "https://github.com/microsoft/regorus"
|
||||
keywords = ["interpreter", "no_std", "opa", "policy-as-code", "rego"]
|
||||
|
||||
# Enable verification with Verus
|
||||
|
||||
[package.metadata.verus]
|
||||
verify = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
@@ -24,8 +29,8 @@ default = ["full-opa", "arc", "rvm"]
|
||||
|
||||
arc = []
|
||||
ast = []
|
||||
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
|
||||
azure-rbac = []
|
||||
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"]
|
||||
azure-rbac = ["regex", "time", "net"]
|
||||
base64 = ["dep:data-encoding"]
|
||||
base64url = ["dep:data-encoding"]
|
||||
coverage = []
|
||||
@@ -39,10 +44,11 @@ net = ["dep:ipnet"]
|
||||
no_std = ["lazy_static/spin_no_std"]
|
||||
opa-runtime = []
|
||||
regex = ["dep:regex"]
|
||||
rvm = ["dep:bincode", "dep:indexmap"]
|
||||
cache = ["dep:lru"]
|
||||
rvm = ["dep:postcard", "dep:indexmap"]
|
||||
semver = ["dep:semver"]
|
||||
allocator-memory-limits = ["std", "mimalloc", "mimalloc/allocator-memory-limits"]
|
||||
std = ["rand/std", "rand/std_rng", "serde_json/std", "msvc_spectre_libs" ]
|
||||
std = ["rand/std", "rand/std_rng", "serde_json/std", "indexmap?/std", "msvc_spectre_libs", "dep:parking_lot" ]
|
||||
time = ["dep:chrono", "dep:chrono-tz"]
|
||||
uuid = ["dep:uuid"]
|
||||
urlquery = ["dep:url"]
|
||||
@@ -56,11 +62,10 @@ full-opa = [
|
||||
"hex",
|
||||
"http",
|
||||
"jsonschema",
|
||||
"allocator-memory-limits",
|
||||
"mimalloc",
|
||||
"net",
|
||||
"opa-runtime",
|
||||
"regex",
|
||||
"cache",
|
||||
"semver",
|
||||
"std",
|
||||
"time",
|
||||
@@ -96,42 +101,49 @@ opa-testutil = []
|
||||
rand = ["dep:rand"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.45", default-features = false }
|
||||
anyhow = { version = "1.0.102", default-features = false }
|
||||
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
|
||||
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
|
||||
hashbrown = { version = "0.16", default-features = false, features = ["default-hasher"], optional = true }
|
||||
lazy_static = { version = "1.4.0", default-features = false }
|
||||
thiserror = { version = "2.0", default-features = false }
|
||||
|
||||
data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] }
|
||||
num-bigint = { version = "0.4", default-features = false }
|
||||
num-traits = { version = "0.2", default-features = false }
|
||||
spin = { version = "0.9.8", default-features = false, features = ["mutex", "spin_mutex"] }
|
||||
parking_lot = { version = "0.12", optional = true }
|
||||
spin = { version = "0.10.0", default-features = false, features = ["mutex", "spin_mutex"] }
|
||||
|
||||
globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true }
|
||||
regex = {version = "1.11.1", optional = true, default-features = false }
|
||||
regex = {version = "1.12.3", optional = true, default-features = false }
|
||||
semver = {version = "1.0.25", optional = true, default-features = false }
|
||||
url = { version = "2.5.4", optional = true }
|
||||
uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng"], optional = true }
|
||||
jsonschema = { version = "0.30.0", default-features = false, optional = true }
|
||||
chrono = { version = "0.4.40", optional = true }
|
||||
uuid = { version = "1.22.0", default-features = false, features = ["v4", "fast-rng"], optional = true }
|
||||
jsonschema = { version = "0.45.0", default-features = false, optional = true }
|
||||
chrono = { version = "0.4.44", optional = true }
|
||||
chrono-tz = { version = "0.10.1", optional = true }
|
||||
ipnet = { version = "2.11.0", optional = true, default-features = false }
|
||||
ipnet = { version = "2.12.0", optional = true, default-features = false }
|
||||
icu_casemap = { version = "2.1", optional = true, default-features = false, features = ["compiled_data"] }
|
||||
|
||||
serde_yaml = {version = "0.9.16", default-features = false, optional = true }
|
||||
# Specify thread_rng for in order to use random_range
|
||||
rand = { version = "0.9.0", default-features = false, features = ["thread_rng"], optional = true }
|
||||
rand = { version = "0.10.0", default-features = false, features = ["thread_rng"], optional = true }
|
||||
|
||||
# Causes the project to link with the Spectre-mitigated CRT and libs.
|
||||
msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true }
|
||||
dashmap = { version = "6.1", default-features = false, optional = true }
|
||||
lru = { version = "0.16", default-features = false, optional = true }
|
||||
mimalloc = { package = "regorus-mimalloc", path = "mimalloc", version = "2.2.6", optional = true }
|
||||
|
||||
# rvm related deps
|
||||
indexmap = { version = "2.12.1", default-features = false, features = ["serde"], optional = true }
|
||||
bincode = { version = "2.0.1", default-features = false, features = ["alloc", "serde"], optional = true }
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"], optional = true }
|
||||
|
||||
# Use Verus for verification
|
||||
vstd = { version = "0.0.0-2026-03-17-2326" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.45"
|
||||
anyhow = "1.0.102"
|
||||
cfg-if = "1.0.0"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
prettydiff = { version = "0.9.0", default-features = false }
|
||||
@@ -189,6 +201,16 @@ harness = false
|
||||
name = "aci_benchmark"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "rvm_benchmark"
|
||||
harness = false
|
||||
required-features = ["rvm"]
|
||||
|
||||
[[bench]]
|
||||
name = "normalization_benchmark"
|
||||
harness = false
|
||||
required-features = ["azure_policy"]
|
||||
|
||||
[[example]]
|
||||
name="regorus"
|
||||
harness=false
|
||||
@@ -200,3 +222,6 @@ doctest=false
|
||||
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(verus_keep_ghost)'] }
|
||||
|
||||
560
benches/normalization_benchmark.rs
Normal file
560
benches/normalization_benchmark.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use regorus::languages::azure_policy::aliases::{denormalizer, normalizer, AliasRegistry};
|
||||
use regorus::Value;
|
||||
use serde_json::json;
|
||||
|
||||
// ─── Alias catalog (reused across benchmarks) ───────────────────────────────
|
||||
|
||||
const ALIASES_JSON: &str = r#"[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority",
|
||||
"defaultPath": "properties.securityRules[*].properties.priority",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction",
|
||||
"defaultPath": "properties.securityRules[*].properties.direction",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix",
|
||||
"defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange",
|
||||
"defaultPath": "properties.securityRules[*].properties.destinationPortRange",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules[*].protocol",
|
||||
"defaultPath": "properties.defaultSecurityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
|
||||
"defaultPath": "properties.minimumTlsVersion",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
|
||||
"defaultPath": "properties.allowBlobPublicAccess",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/sku.name",
|
||||
"defaultPath": "sku.name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
fn build_registry() -> AliasRegistry {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(ALIASES_JSON).unwrap();
|
||||
reg
|
||||
}
|
||||
|
||||
/// Convert a serde_json::Value to regorus::Value.
|
||||
fn to_regorus(v: serde_json::Value) -> Value {
|
||||
Value::from(v)
|
||||
}
|
||||
|
||||
// ─── Input resources ────────────────────────────────────────────────────────
|
||||
|
||||
fn simple_storage_resource() -> Value {
|
||||
to_regorus(json!({
|
||||
"name": "myStorageAccount",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"location": "westus2",
|
||||
"kind": "StorageV2",
|
||||
"sku": { "name": "Standard_LRS", "tier": "Standard" },
|
||||
"tags": { "environment": "production", "team": "platform" },
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"accessTier": "Hot",
|
||||
"isHnsEnabled": false,
|
||||
"minimumTlsVersion": "TLS1_2",
|
||||
"allowBlobPublicAccess": false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn nsg_resource(rule_count: usize) -> Value {
|
||||
let rules: Vec<serde_json::Value> = (0..rule_count)
|
||||
.map(|i| {
|
||||
json!({
|
||||
"name": format!("rule-{}", i),
|
||||
"properties": {
|
||||
"protocol": "Tcp",
|
||||
"access": if i % 2 == 0 { "Allow" } else { "Deny" },
|
||||
"priority": 100 + i,
|
||||
"direction": "Inbound",
|
||||
"sourceAddressPrefix": format!("10.0.{}.0/24", i % 256),
|
||||
"destinationPortRange": format!("{}", 80 + i)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
to_regorus(json!({
|
||||
"name": "myNsg",
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"location": "eastus",
|
||||
"properties": {
|
||||
"securityRules": rules
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ─── Benchmarks ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn bench_normalize_simple(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let resource = simple_storage_resource();
|
||||
|
||||
c.bench_function("normalize/simple_storage", |b| {
|
||||
b.iter(|| normalizer::normalize(black_box(&resource), Some(®istry), 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(®istry), 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(®istry), None);
|
||||
|
||||
c.bench_function("denormalize/simple_storage", |b| {
|
||||
b.iter(|| denormalizer::denormalize(black_box(&normalized), Some(®istry), 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(®istry), None);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&normalized,
|
||||
|b, norm| b.iter(|| denormalizer::denormalize(black_box(norm), Some(®istry), 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(®istry), None);
|
||||
denormalizer::denormalize(&n, Some(®istry), 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(®), 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(®istry), None);
|
||||
denormalizer::denormalize(&n, Some(®istry), 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(®istry), 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(®istry), Some("2020-01-01"));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&normalized,
|
||||
|b, norm| {
|
||||
b.iter(|| {
|
||||
denormalizer::denormalize(black_box(norm), Some(®istry), 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(®istry), Some("2020-01-01"));
|
||||
denormalizer::denormalize(&n, Some(®istry), Some("2020-01-01"))
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
normalization_benches,
|
||||
bench_normalize_simple,
|
||||
bench_normalize_no_aliases,
|
||||
bench_normalize_nsg_scaling,
|
||||
bench_denormalize_simple,
|
||||
bench_denormalize_nsg_scaling,
|
||||
bench_round_trip,
|
||||
bench_normalize_and_wrap,
|
||||
bench_registry_load,
|
||||
bench_normalize_large_catalog,
|
||||
bench_denormalize_large_catalog,
|
||||
bench_nsg_large_subarrays,
|
||||
bench_normalize_versioned,
|
||||
bench_denormalize_versioned,
|
||||
bench_round_trip_versioned,
|
||||
);
|
||||
criterion_main!(normalization_benches);
|
||||
680
benches/rvm_benchmark.rs
Normal file
680
benches/rvm_benchmark.rs
Normal file
@@ -0,0 +1,680 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Comprehensive RVM benchmarks covering all aspects of the Rego Virtual Machine.
|
||||
//!
|
||||
//! # Policy families
|
||||
//!
|
||||
//! | Family | Source | Policies | Inputs/policy |
|
||||
//! |------------|-------------------------------|----------|---------------|
|
||||
//! | Synthetic | `benches/evaluation/test_data`| 9 | 3 each |
|
||||
//! | ACI | `tests/aci` | 9 | 1 each |
|
||||
//!
|
||||
//! # Benchmark groups
|
||||
//!
|
||||
//! | Group | What it measures |
|
||||
//! |--------------------------|-------------------------------------------------------|
|
||||
//! | `cold/{case}/{config}` | Cold: new VM + load + data + input + execute |
|
||||
//! | `hot/{case}/{config}` | Hot: set_input + execute (VM reused across iters) |
|
||||
//! | `compilation` | Rego CompiledPolicy → RVM Program |
|
||||
//! | `serialization` | Program binary serialize / deserialize roundtrip |
|
||||
//! | `startup` | Isolated VM creation & setup overhead |
|
||||
//! | `stats` | Instruction/literal counts (reported as throughput) |
|
||||
//! | `end_to_end` | Full roundtrip: compile → serialize → deserialize → eval |
|
||||
//!
|
||||
//! # Running subsets
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo bench --bench rvm_benchmark # everything
|
||||
//! cargo bench --bench rvm_benchmark -- cold # all cold eval
|
||||
//! cargo bench --bench rvm_benchmark -- hot # all hot eval
|
||||
//! cargo bench --bench rvm_benchmark -- regular_with_limits # one config across cases
|
||||
//! cargo bench --bench rvm_benchmark -- cold/aci/ # all ACI cold benchmarks
|
||||
//! cargo bench --bench rvm_benchmark -- rbac # one policy family
|
||||
//! cargo bench --bench rvm_benchmark -- compilation # compilation only
|
||||
//! cargo bench --bench rvm_benchmark -- serialization # serialization only
|
||||
//! cargo bench --bench rvm_benchmark -- startup # startup overhead
|
||||
//! ```
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use regorus::languages::rego::compiler::Compiler;
|
||||
use regorus::rvm::program::Program;
|
||||
use regorus::rvm::vm::{ExecutionMode, RegoVM};
|
||||
use regorus::utils::limits::ExecutionTimerConfig;
|
||||
use regorus::{Engine, Rc, Value};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Limit constants – generous ceilings that still exercise the limit-checking
|
||||
// hot path (memory_check, execution_timer_tick, instruction-limit compare).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
const MEMORY_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
|
||||
const TIME_LIMIT: Duration = Duration::from_secs(30);
|
||||
const TIMER_CHECK_INTERVAL: NonZeroU32 = NonZeroU32::new(16).unwrap();
|
||||
const INSTRUCTION_LIMIT: usize = 10_000_000;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct EvalConfig {
|
||||
name: &'static str,
|
||||
mode: ExecutionMode,
|
||||
limits: bool,
|
||||
}
|
||||
|
||||
const EVAL_CONFIGS: [EvalConfig; 4] = [
|
||||
EvalConfig {
|
||||
name: "regular_no_limits",
|
||||
mode: ExecutionMode::RunToCompletion,
|
||||
limits: false,
|
||||
},
|
||||
EvalConfig {
|
||||
name: "regular_with_limits",
|
||||
mode: ExecutionMode::RunToCompletion,
|
||||
limits: true,
|
||||
},
|
||||
EvalConfig {
|
||||
name: "suspendable_no_limits",
|
||||
mode: ExecutionMode::Suspendable,
|
||||
limits: false,
|
||||
},
|
||||
EvalConfig {
|
||||
name: "suspendable_with_limits",
|
||||
mode: ExecutionMode::Suspendable,
|
||||
limits: true,
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A compiled benchmark program ready for RVM execution.
|
||||
struct BenchmarkProgram {
|
||||
/// Human-readable name (e.g. "rbac_policy" or "aci/create_container").
|
||||
name: String,
|
||||
/// Pre-compiled RVM program.
|
||||
program: Arc<Program>,
|
||||
/// Compiled policy (kept for compilation benchmarks).
|
||||
compiled_policy: regorus::CompiledPolicy,
|
||||
/// Entry-point rule path.
|
||||
entry_point: String,
|
||||
/// Data object (Some for policies that require external data like ACI).
|
||||
data: Option<Value>,
|
||||
/// Named inputs for this policy.
|
||||
inputs: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACI YAML types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct AciTestCase {
|
||||
note: String,
|
||||
data: Value,
|
||||
input: Value,
|
||||
modules: Vec<String>,
|
||||
query: String,
|
||||
want_result: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct AciYamlTest {
|
||||
cases: Vec<AciTestCase>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Synthetic policy loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Policy ↔ input file mapping for synthetic policies.
|
||||
const SYNTHETIC_POLICIES: &[(&str, &str, &[&str])] = &[
|
||||
(
|
||||
"rbac_policy",
|
||||
"rbac_policy.rego",
|
||||
&["rbac_input.json", "rbac_input2.json", "rbac_input3.json"],
|
||||
),
|
||||
(
|
||||
"api_access",
|
||||
"api_access_policy.rego",
|
||||
&[
|
||||
"api_access_input.json",
|
||||
"api_access_input2.json",
|
||||
"api_access_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"data_sensitivity",
|
||||
"data_sensitivity_policy.rego",
|
||||
&[
|
||||
"data_sensitivity_input.json",
|
||||
"data_sensitivity_input2.json",
|
||||
"data_sensitivity_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"time_based",
|
||||
"time_based_policy.rego",
|
||||
&[
|
||||
"time_based_input.json",
|
||||
"time_based_input2.json",
|
||||
"time_based_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"data_processing",
|
||||
"data_processing_policy.rego",
|
||||
&[
|
||||
"data_processing_input.json",
|
||||
"data_processing_input2.json",
|
||||
"data_processing_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"azure_vm",
|
||||
"azure_vm_policy.rego",
|
||||
&[
|
||||
"azure_vm_input.json",
|
||||
"azure_vm_input2.json",
|
||||
"azure_vm_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"azure_storage",
|
||||
"azure_storage_policy.rego",
|
||||
&[
|
||||
"azure_storage_input.json",
|
||||
"azure_storage_input2.json",
|
||||
"azure_storage_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"azure_keyvault",
|
||||
"azure_keyvault_policy.rego",
|
||||
&[
|
||||
"azure_keyvault_input.json",
|
||||
"azure_keyvault_input2.json",
|
||||
"azure_keyvault_input3.json",
|
||||
],
|
||||
),
|
||||
(
|
||||
"azure_nsg",
|
||||
"azure_nsg_policy.rego",
|
||||
&[
|
||||
"azure_nsg_input.json",
|
||||
"azure_nsg_input2.json",
|
||||
"azure_nsg_input3.json",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// Compile synthetic Rego policies into RVM programs.
|
||||
fn compile_synthetic_programs() -> Vec<BenchmarkProgram> {
|
||||
let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("benches")
|
||||
.join("evaluation")
|
||||
.join("test_data");
|
||||
|
||||
let entry_point = "data.bench.allow";
|
||||
let entry_point_rc: Rc<str> = entry_point.into();
|
||||
|
||||
SYNTHETIC_POLICIES
|
||||
.iter()
|
||||
.map(|(name, policy_file, input_files)| {
|
||||
let policy_path = base_dir.join("policies").join(policy_file);
|
||||
let policy_content = std::fs::read_to_string(&policy_path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read {policy_path:?}: {e}"));
|
||||
|
||||
let mut engine = Engine::new();
|
||||
engine
|
||||
.add_policy("policy.rego".to_string(), policy_content)
|
||||
.expect("failed to add policy");
|
||||
|
||||
let compiled_policy = engine
|
||||
.compile_with_entrypoint(&entry_point_rc)
|
||||
.expect("failed to compile policy");
|
||||
|
||||
let program = Compiler::compile_from_policy(&compiled_policy, &[entry_point])
|
||||
.expect("failed to compile to RVM program");
|
||||
|
||||
let inputs: Vec<(String, Value)> = input_files
|
||||
.iter()
|
||||
.map(|input_file| {
|
||||
let input_path = base_dir.join("inputs").join(input_file);
|
||||
let json = std::fs::read_to_string(&input_path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read {input_path:?}: {e}"));
|
||||
let value = Value::from_json_str(&json).expect("failed to parse input JSON");
|
||||
let display = input_file.trim_end_matches(".json").to_string();
|
||||
(display, value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
BenchmarkProgram {
|
||||
name: name.to_string(),
|
||||
program,
|
||||
compiled_policy,
|
||||
entry_point: entry_point.to_string(),
|
||||
data: None,
|
||||
inputs,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACI policy loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load all ACI test cases from YAML files.
|
||||
fn load_aci_cases(dir: &Path) -> Vec<AciTestCase> {
|
||||
let mut cases = Vec::new();
|
||||
for entry in WalkDir::new(dir)
|
||||
.sort_by_file_name()
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.to_string_lossy().ends_with(".yaml") {
|
||||
continue;
|
||||
}
|
||||
let yaml = std::fs::read(path).expect("failed to read yaml");
|
||||
let yaml = String::from_utf8_lossy(&yaml);
|
||||
let test: AciYamlTest = serde_yaml::from_str(&yaml).expect("failed to deserialize yaml");
|
||||
cases.extend(test.cases);
|
||||
}
|
||||
cases
|
||||
}
|
||||
|
||||
/// Build an Engine with policies loaded for a given ACI test case.
|
||||
fn build_aci_engine(dir: &Path, case: &AciTestCase) -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
engine.set_rego_v0(true);
|
||||
engine
|
||||
.add_data(case.data.clone())
|
||||
.expect("failed to add data");
|
||||
engine.set_input(case.input.clone());
|
||||
for (idx, rego) in case.modules.iter().enumerate() {
|
||||
if rego.ends_with(".rego") {
|
||||
engine
|
||||
.add_policy_from_file(dir.join(rego).to_str().expect("invalid path"))
|
||||
.expect("failed to add policy");
|
||||
} else {
|
||||
engine
|
||||
.add_policy(format!("rego{idx}.rego"), rego.clone())
|
||||
.expect("failed to add policy");
|
||||
}
|
||||
}
|
||||
engine
|
||||
}
|
||||
|
||||
/// Compile ACI test cases into RVM programs.
|
||||
fn compile_aci_programs() -> Vec<BenchmarkProgram> {
|
||||
let dir = Path::new("tests/aci");
|
||||
load_aci_cases(dir)
|
||||
.into_iter()
|
||||
.map(|case| {
|
||||
let mut engine = build_aci_engine(dir, &case);
|
||||
let rule = case.query.replace("=x", "");
|
||||
let rule_rc: Rc<str> = rule.clone().into();
|
||||
let compiled_policy = engine
|
||||
.compile_with_entrypoint(&rule_rc)
|
||||
.expect("failed to compile");
|
||||
let program = Compiler::compile_from_policy(&compiled_policy, &[rule.as_str()])
|
||||
.expect("failed to compile to RVM");
|
||||
|
||||
BenchmarkProgram {
|
||||
name: format!("aci/{}", case.note),
|
||||
program,
|
||||
compiled_policy,
|
||||
entry_point: rule,
|
||||
data: Some(case.data),
|
||||
inputs: vec![("input".to_string(), case.input)],
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compile all policies
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compile all policies (synthetic + ACI) into RVM programs.
|
||||
fn compile_all_programs() -> Vec<BenchmarkProgram> {
|
||||
let mut programs = compile_synthetic_programs();
|
||||
programs.extend(compile_aci_programs());
|
||||
programs
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Limit helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Apply or remove production-style limits based on a boolean flag.
|
||||
fn configure_limits(vm: &mut RegoVM, limits: bool) {
|
||||
if limits {
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
regorus::set_global_memory_limit(Some(MEMORY_LIMIT_BYTES));
|
||||
vm.set_execution_timer_config(Some(ExecutionTimerConfig {
|
||||
limit: TIME_LIMIT,
|
||||
check_interval: TIMER_CHECK_INTERVAL,
|
||||
}));
|
||||
vm.set_max_instructions(INSTRUCTION_LIMIT);
|
||||
} else {
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
regorus::set_global_memory_limit(None);
|
||||
vm.set_execution_timer_config(None);
|
||||
vm.set_max_instructions(usize::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cold evaluation — new VM per iteration (full setup + execute)
|
||||
//
|
||||
// Benchmarks are registered case-first so each workload is shown with all
|
||||
// config variants adjacent to one another, making per-case comparisons easier.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_cold(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
let mut group = c.benchmark_group("cold");
|
||||
|
||||
for bp in &programs {
|
||||
for (input_name, input_value) in &bp.inputs {
|
||||
let case_id = if bp.inputs.len() == 1 {
|
||||
bp.name.clone()
|
||||
} else {
|
||||
format!("{}/{}", bp.name, input_name)
|
||||
};
|
||||
let program = bp.program.clone();
|
||||
let data = bp.data.clone();
|
||||
let input = input_value.clone();
|
||||
|
||||
for config in EVAL_CONFIGS {
|
||||
group.bench_function(BenchmarkId::new(&case_id, config.name), |b| {
|
||||
b.iter(|| {
|
||||
let mut vm = RegoVM::new();
|
||||
vm.set_execution_mode(config.mode);
|
||||
vm.load_program(black_box(program.clone()));
|
||||
if let Some(ref d) = data {
|
||||
vm.set_data(black_box(d.clone())).unwrap();
|
||||
}
|
||||
vm.set_input(black_box(input.clone()));
|
||||
configure_limits(&mut vm, config.limits);
|
||||
black_box(vm.execute().unwrap())
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hot evaluation — VM reused across iterations
|
||||
//
|
||||
// The VM is created once with program, data, mode, and limits. Each
|
||||
// iteration only calls set_input + execute, measuring pure execution
|
||||
// overhead with minimal setup. A warm-up execution fills the register
|
||||
// window pool so all iterations benefit from pooled allocations.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_hot(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
let mut group = c.benchmark_group("hot");
|
||||
|
||||
for bp in &programs {
|
||||
let program = bp.program.clone();
|
||||
let data = bp.data.clone();
|
||||
let inputs: Vec<Value> = bp.inputs.iter().map(|(_, v)| v.clone()).collect();
|
||||
let num_inputs = inputs.len();
|
||||
|
||||
for config in EVAL_CONFIGS {
|
||||
group.bench_function(BenchmarkId::new(&bp.name, config.name), |b| {
|
||||
let mut vm = RegoVM::new();
|
||||
vm.set_execution_mode(config.mode);
|
||||
vm.load_program(program.clone());
|
||||
if let Some(ref d) = data {
|
||||
vm.set_data(d.clone()).unwrap();
|
||||
}
|
||||
configure_limits(&mut vm, config.limits);
|
||||
|
||||
// Warm up: fill register window pools, caches, etc.
|
||||
vm.set_input(inputs[0].clone());
|
||||
vm.execute().expect("warm-up failed");
|
||||
|
||||
let mut i = 0usize;
|
||||
b.iter(|| {
|
||||
let input = &inputs[i % num_inputs];
|
||||
vm.set_input(black_box(input.clone()));
|
||||
black_box(vm.execute().unwrap());
|
||||
i += 1;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compilation — Rego CompiledPolicy → RVM Program
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_compilation(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
let mut group = c.benchmark_group("compilation");
|
||||
|
||||
for bp in &programs {
|
||||
let entry_point: &str = &bp.entry_point;
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("rego_to_rvm", &bp.name),
|
||||
&bp.compiled_policy,
|
||||
|b, compiled_policy| {
|
||||
b.iter(|| {
|
||||
Compiler::compile_from_policy(
|
||||
black_box(compiled_policy),
|
||||
black_box(&[entry_point]),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Serialization — binary serialize / deserialize roundtrip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_serialization(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
let mut group = c.benchmark_group("serialization");
|
||||
|
||||
for bp in &programs {
|
||||
let program = &bp.program;
|
||||
let serialized = program
|
||||
.serialize_binary()
|
||||
.expect("failed to serialize program");
|
||||
let byte_len = serialized.len() as u64;
|
||||
|
||||
group.throughput(Throughput::Bytes(byte_len));
|
||||
group.bench_function(BenchmarkId::new("serialize", &bp.name), |b| {
|
||||
b.iter(|| black_box(program.serialize_binary().unwrap()))
|
||||
});
|
||||
|
||||
group.throughput(Throughput::Bytes(byte_len));
|
||||
group.bench_function(BenchmarkId::new("deserialize", &bp.name), |b| {
|
||||
b.iter(|| black_box(Program::deserialize_binary(black_box(&serialized)).unwrap()))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Startup — isolated VM creation & setup overhead
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_startup(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
let mut group = c.benchmark_group("startup");
|
||||
|
||||
// Use the first program as representative for startup overhead.
|
||||
let bp = &programs[0];
|
||||
let program = bp.program.clone();
|
||||
let input = bp.inputs[0].1.clone();
|
||||
|
||||
// Bare VM creation
|
||||
group.bench_function("new", |b| b.iter(|| black_box(RegoVM::new())));
|
||||
|
||||
// load_program (Arc clone + internal setup)
|
||||
group.bench_function("load_program", |b| {
|
||||
b.iter(|| {
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(black_box(program.clone()));
|
||||
black_box(&vm);
|
||||
})
|
||||
});
|
||||
|
||||
// set_input
|
||||
group.bench_function("set_input", |b| {
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(program.clone());
|
||||
b.iter(|| {
|
||||
vm.set_input(black_box(input.clone()));
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stats — instruction / literal counts (reported as throughput)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_stats(c: &mut Criterion) {
|
||||
let programs = compile_all_programs();
|
||||
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"{:<30} {:>8} {:>8} {:>8} {:>10}",
|
||||
"program", "instrs", "lits", "entries", "bytes"
|
||||
);
|
||||
eprintln!("{}", "-".repeat(70));
|
||||
|
||||
let mut group = c.benchmark_group("stats");
|
||||
for bp in &programs {
|
||||
let serialized = bp.program.serialize_binary().expect("serialize failed");
|
||||
let byte_len = serialized.len();
|
||||
let instr_count = bp.program.instructions.len();
|
||||
let lit_count = bp.program.literals.len();
|
||||
let entry_count = bp.program.entry_points.len();
|
||||
|
||||
eprintln!(
|
||||
"{:<30} {:>8} {:>8} {:>8} {:>10}",
|
||||
bp.name, instr_count, lit_count, entry_count, byte_len,
|
||||
);
|
||||
|
||||
group.throughput(Throughput::Elements(instr_count as u64));
|
||||
group.bench_function(BenchmarkId::new("serialize", &bp.name), |b| {
|
||||
b.iter(|| black_box(bp.program.serialize_binary().unwrap()))
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// End-to-end roundtrip (compile + serialize + deserialize + eval)
|
||||
//
|
||||
// Only runs for synthetic policies where we have direct access to rego
|
||||
// source files. ACI policies are loaded from YAML with module references
|
||||
// which makes the setup pipeline different.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_end_to_end(c: &mut Criterion) {
|
||||
let base_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("benches")
|
||||
.join("evaluation")
|
||||
.join("test_data");
|
||||
|
||||
let entry_point = "data.bench.allow";
|
||||
let entry_point_rc: Rc<str> = entry_point.into();
|
||||
|
||||
let mut group = c.benchmark_group("end_to_end");
|
||||
|
||||
for &(name, policy_file, input_files) in SYNTHETIC_POLICIES {
|
||||
let policy_path = base_dir.join("policies").join(policy_file);
|
||||
let policy_content = std::fs::read_to_string(&policy_path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read {policy_path:?}: {e}"));
|
||||
|
||||
// Use just the first input for end-to-end
|
||||
let input_path = base_dir.join("inputs").join(input_files[0]);
|
||||
let input_json = std::fs::read_to_string(&input_path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read {input_path:?}: {e}"));
|
||||
|
||||
group.bench_function(BenchmarkId::new("roundtrip", name), |b| {
|
||||
b.iter(|| {
|
||||
// 1. Engine + parse
|
||||
let mut engine = Engine::new();
|
||||
engine
|
||||
.add_policy("policy.rego".to_string(), policy_content.clone())
|
||||
.unwrap();
|
||||
|
||||
// 2. Compile to CompiledPolicy
|
||||
let compiled_policy = engine.compile_with_entrypoint(&entry_point_rc).unwrap();
|
||||
|
||||
// 3. Compile to RVM Program
|
||||
let program =
|
||||
Compiler::compile_from_policy(&compiled_policy, &[entry_point]).unwrap();
|
||||
|
||||
// 4. Serialize
|
||||
let bytes = program.serialize_binary().unwrap();
|
||||
|
||||
// 5. Deserialize
|
||||
let deserialized = Program::deserialize_binary(&bytes).unwrap();
|
||||
let program = match deserialized {
|
||||
regorus::rvm::program::DeserializationResult::Complete(p) => Arc::new(p),
|
||||
regorus::rvm::program::DeserializationResult::Partial(p) => {
|
||||
Arc::new(Program::compile_from_partial(p).unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Execute
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(program);
|
||||
let input = Value::from_json_str(&input_json).unwrap();
|
||||
vm.set_input(input);
|
||||
black_box(vm.execute().unwrap());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Criterion groups — organised for selective runs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
criterion_group!(cold_benches, bench_cold);
|
||||
|
||||
criterion_group!(hot_benches, bench_hot);
|
||||
|
||||
criterion_group!(
|
||||
misc_benches,
|
||||
bench_compilation,
|
||||
bench_serialization,
|
||||
bench_startup,
|
||||
bench_stats,
|
||||
bench_end_to_end,
|
||||
);
|
||||
|
||||
criterion_main!(cold_benches, hot_benches, misc_benches);
|
||||
@@ -11,6 +11,20 @@ int main() {
|
||||
if (r.status != Ok)
|
||||
goto error;
|
||||
|
||||
// Configure the global pattern caches.
|
||||
RegorusCacheConfig cache_config = { .regex = 256, .glob = 128 };
|
||||
r = regorus_set_cache_config(cache_config);
|
||||
if (r.status != Ok)
|
||||
goto error;
|
||||
regorus_result_drop(r);
|
||||
|
||||
// Raise the default col limit to 2000
|
||||
RegorusPolicyLengthConfig len_config = { .max_col = 2000, .max_file_bytes = 1048576, .max_lines = 20000 };
|
||||
r = regorus_engine_set_policy_length_config(engine, len_config);
|
||||
if (r.status != Ok)
|
||||
goto error;
|
||||
regorus_result_drop(r);
|
||||
|
||||
// Load policies.
|
||||
r = regorus_engine_add_policy_from_file(engine, "../../../tests/aci/framework.rego");
|
||||
if (r.status != Ok)
|
||||
|
||||
@@ -6,8 +6,19 @@ void example()
|
||||
// Create engine
|
||||
regorus::Engine engine;
|
||||
|
||||
// Configure the global pattern caches.
|
||||
RegorusCacheConfig cache_config = { 256, 128 };
|
||||
regorus::set_cache_config(cache_config);
|
||||
|
||||
engine.set_rego_v0(true);
|
||||
engine.set_enable_coverage(true);
|
||||
|
||||
RegorusPolicyLengthConfig len_config;
|
||||
// Raise the default col limit to 2000
|
||||
len_config.max_col = 2000;
|
||||
len_config.max_file_bytes = 1048576;
|
||||
len_config.max_lines = 20000;
|
||||
engine.set_policy_length_config(len_config);
|
||||
|
||||
// Add policies.
|
||||
engine.add_policy("objects.rego",R"(package objects
|
||||
|
||||
@@ -131,7 +131,15 @@ 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);
|
||||
}
|
||||
@@ -150,6 +158,14 @@ 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) {}
|
||||
|
||||
@@ -10,5 +10,6 @@
|
||||
<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>
|
||||
|
||||
@@ -104,3 +104,49 @@ 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}");
|
||||
```
|
||||
|
||||
191
bindings/csharp/Regorus.Tests/AliasRegistryTests.cs
Normal file
191
bindings/csharp/Regorus.Tests/AliasRegistryTests.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Regorus;
|
||||
|
||||
namespace Regorus.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class AliasRegistryTests
|
||||
{
|
||||
private const string AliasesJson = @"[{
|
||||
""namespace"": ""Microsoft.Storage"",
|
||||
""resourceTypes"": [{
|
||||
""resourceType"": ""storageAccounts"",
|
||||
""aliases"": [{
|
||||
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
|
||||
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
|
||||
""paths"": []
|
||||
}, {
|
||||
""name"": ""Microsoft.Storage/storageAccounts/accessTier"",
|
||||
""defaultPath"": ""properties.accessTier"",
|
||||
""paths"": []
|
||||
}]
|
||||
}]
|
||||
}]";
|
||||
|
||||
private const string ManifestJson = @"{
|
||||
""dataNamespace"": ""Microsoft.KeyVault.Data"",
|
||||
""aliases"": [],
|
||||
""resourceTypeAliases"": [{
|
||||
""resourceType"": ""vaults/certificates"",
|
||||
""aliases"": [{
|
||||
""name"": ""Microsoft.KeyVault.Data/vaults/certificates/keySize"",
|
||||
""paths"": [{ ""path"": ""keySize"", ""apiVersions"": [""7.0""] }]
|
||||
}]
|
||||
}]
|
||||
}";
|
||||
|
||||
[TestMethod]
|
||||
public void Create_and_dispose_succeeds()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
Assert.AreEqual(0, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoadJson_populates_registry()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
Assert.AreEqual(1, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoadManifest_populates_registry()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadManifest(ManifestJson);
|
||||
Assert.AreEqual(1, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NormalizeAndWrap_produces_envelope()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
|
||||
}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.IsNotNull(envelope["resource"]);
|
||||
Assert.IsNotNull(envelope["parameters"]);
|
||||
Assert.IsNotNull(envelope["context"]);
|
||||
|
||||
// Normalized resource should have lowercased alias field names
|
||||
var res = envelope["resource"]!;
|
||||
Assert.AreEqual(true, res["supportshttpstrafficonly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", res["accesstier"]?.GetValue<string>());
|
||||
Assert.AreEqual("acct1", res["name"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NormalizeAndWrap_with_context_and_parameters()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true }
|
||||
}";
|
||||
var context = @"{""resourceGroup"": {""name"": ""rg1""}}";
|
||||
var parameters = @"{""env"": ""prod""}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "2023-01-01", context, parameters);
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual("rg1", envelope["context"]!["resourceGroup"]!["name"]?.GetValue<string>());
|
||||
Assert.AreEqual("prod", envelope["parameters"]!["env"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Denormalize_restores_properties()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var normalized = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""supportshttpstrafficonly"": true,
|
||||
""accesstier"": ""Hot""
|
||||
}";
|
||||
|
||||
var result = registry.Denormalize(normalized, "2023-01-01");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var arm = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
|
||||
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Round_trip_normalize_then_denormalize()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
|
||||
}";
|
||||
|
||||
// Normalize
|
||||
var envelopeJson = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
|
||||
Assert.IsNotNull(envelopeJson);
|
||||
|
||||
var envelope = JsonNode.Parse(envelopeJson!)!;
|
||||
var normalizedResource = envelope["resource"]!.ToJsonString();
|
||||
|
||||
// Denormalize
|
||||
var armJson = registry.Denormalize(normalizedResource, "2023-01-01");
|
||||
Assert.IsNotNull(armJson);
|
||||
|
||||
var arm = JsonNode.Parse(armJson!)!;
|
||||
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
|
||||
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DataPlane_manifest_normalize()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadManifest(ManifestJson);
|
||||
|
||||
var resource = @"{
|
||||
""type"": ""Microsoft.KeyVault.Data/vaults/certificates"",
|
||||
""keySize"": 2048
|
||||
}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "7.0", "{}", "{}");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual(2048, envelope["resource"]!["keysize"]?.GetValue<int>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public void LoadJson_invalid_throws()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson("not valid json");
|
||||
}
|
||||
}
|
||||
374
bindings/csharp/Regorus.Tests/RbacEngineTests.cs
Normal file
374
bindings/csharp/Regorus.Tests/RbacEngineTests.cs
Normal file
@@ -0,0 +1,374 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Regorus;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Regorus.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class RbacEngineTests
|
||||
{
|
||||
public TestContext? TestContext { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private const string BaseContextJson = """
|
||||
{
|
||||
"principal": {
|
||||
"id": "user-1",
|
||||
"principal_type": "User",
|
||||
"custom_security_attributes": {
|
||||
"department": "eng",
|
||||
"levels": ["L1", "L2"]
|
||||
}
|
||||
},
|
||||
"resource": {
|
||||
"id": "/subscriptions/s1",
|
||||
"resource_type": "Microsoft.Storage/storageAccounts",
|
||||
"scope": "/subscriptions/s1",
|
||||
"attributes": {
|
||||
"owner": "alice",
|
||||
"tags": ["a", "b"],
|
||||
"count": 5,
|
||||
"enabled": false,
|
||||
"ip": "10.0.0.5",
|
||||
"guid": "a1b2c3d4-0000-0000-0000-000000000000"
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"action": "Microsoft.Storage/storageAccounts/read",
|
||||
"data_action": "Microsoft.Storage/storageAccounts/read",
|
||||
"attributes": {
|
||||
"owner": "alice",
|
||||
"text": "HelloWorld",
|
||||
"tags": ["prod", "gold"],
|
||||
"count": 10,
|
||||
"ratio": 2.5,
|
||||
"enabled": true,
|
||||
"ip": "10.0.0.8",
|
||||
"guid": "A1B2C3D4-0000-0000-0000-000000000000",
|
||||
"time": "12:30:15",
|
||||
"date": "2023-05-01T12:00:00Z",
|
||||
"numbers": [1, 2, 3],
|
||||
"letters": ["a", "b"]
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"is_private_link": false,
|
||||
"private_endpoint": null,
|
||||
"subnet": null,
|
||||
"utc_now": "2023-05-01T12:00:00Z"
|
||||
},
|
||||
"action": "Microsoft.Storage/storageAccounts/read",
|
||||
"suboperation": "sub/read"
|
||||
}
|
||||
""";
|
||||
|
||||
[TestMethod]
|
||||
public void Rbac_engine_evaluates_all_yaml_cases()
|
||||
{
|
||||
var cases = LoadEvalTestCases().ToList();
|
||||
Assert.IsTrue(cases.Count > 0, "No RBAC test cases were loaded.");
|
||||
|
||||
foreach (var testCase in cases)
|
||||
{
|
||||
TestContext?.WriteLine($"RBAC case: {testCase.Name} -> {testCase.Condition}");
|
||||
var context = BuildBaseContext();
|
||||
if (testCase.Context != null)
|
||||
{
|
||||
ApplyOverrides(context, testCase.Context);
|
||||
}
|
||||
|
||||
var contextJson = context.ToJsonString(JsonOptions);
|
||||
var result = RbacEngine.EvaluateCondition(testCase.Condition, contextJson);
|
||||
|
||||
Assert.AreEqual(
|
||||
testCase.Expected,
|
||||
result,
|
||||
$"RBAC test '{testCase.Name}' failed for condition '{testCase.Condition}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject BuildBaseContext()
|
||||
{
|
||||
var node = JsonNode.Parse(BaseContextJson) as JsonObject;
|
||||
if (node is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse base context JSON.");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static void ApplyOverrides(JsonObject context, EvalContextOverrides overrides)
|
||||
{
|
||||
var principal = (JsonObject?)context["principal"]
|
||||
?? throw new InvalidOperationException("Missing principal section.");
|
||||
var resource = (JsonObject?)context["resource"]
|
||||
?? throw new InvalidOperationException("Missing resource section.");
|
||||
var request = (JsonObject?)context["request"]
|
||||
?? throw new InvalidOperationException("Missing request section.");
|
||||
var environment = (JsonObject?)context["environment"]
|
||||
?? throw new InvalidOperationException("Missing environment section.");
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.Action))
|
||||
{
|
||||
context["action"] = overrides.Action;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.Suboperation))
|
||||
{
|
||||
context["suboperation"] = overrides.Suboperation;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.RequestAction))
|
||||
{
|
||||
request["action"] = overrides.RequestAction;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.DataAction))
|
||||
{
|
||||
request["data_action"] = overrides.DataAction;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.PrincipalId))
|
||||
{
|
||||
principal["id"] = overrides.PrincipalId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.PrincipalType))
|
||||
{
|
||||
principal["principal_type"] = overrides.PrincipalType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.ResourceId))
|
||||
{
|
||||
resource["id"] = overrides.ResourceId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.ResourceType))
|
||||
{
|
||||
resource["resource_type"] = overrides.ResourceType;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.ResourceScope))
|
||||
{
|
||||
resource["scope"] = overrides.ResourceScope;
|
||||
}
|
||||
|
||||
if (overrides.RequestAttributes != null)
|
||||
{
|
||||
request["attributes"] = ConvertToJsonNode(overrides.RequestAttributes);
|
||||
}
|
||||
|
||||
if (overrides.ResourceAttributes != null)
|
||||
{
|
||||
resource["attributes"] = ConvertToJsonNode(overrides.ResourceAttributes);
|
||||
}
|
||||
|
||||
if (overrides.PrincipalCustomSecurityAttributes != null)
|
||||
{
|
||||
principal["custom_security_attributes"] = ConvertToJsonNode(overrides.PrincipalCustomSecurityAttributes);
|
||||
}
|
||||
|
||||
if (overrides.Environment != null)
|
||||
{
|
||||
if (overrides.Environment.IsPrivateLink.HasValue)
|
||||
{
|
||||
environment["is_private_link"] = overrides.Environment.IsPrivateLink.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.Environment.PrivateEndpoint))
|
||||
{
|
||||
environment["private_endpoint"] = overrides.Environment.PrivateEndpoint;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.Environment.Subnet))
|
||||
{
|
||||
environment["subnet"] = overrides.Environment.Subnet;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(overrides.Environment.UtcNow))
|
||||
{
|
||||
environment["utc_now"] = overrides.Environment.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<EvalTestCase> LoadEvalTestCases()
|
||||
{
|
||||
var baseDir = Path.Combine(AppContext.BaseDirectory, "test_cases");
|
||||
if (!Directory.Exists(baseDir))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"RBAC test case directory not found: {baseDir}");
|
||||
}
|
||||
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
|
||||
var files = Directory.EnumerateFiles(baseDir, "*.yaml")
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var yaml = File.ReadAllText(file);
|
||||
var suite = deserializer.Deserialize<EvalTestSuite>(yaml);
|
||||
if (suite?.TestCases is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var testCase in suite.TestCases)
|
||||
{
|
||||
yield return testCase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonNode? ConvertToJsonNode(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case JsonNode node:
|
||||
return node;
|
||||
case string text:
|
||||
return JsonValue.Create(text);
|
||||
case bool boolean:
|
||||
return JsonValue.Create(boolean);
|
||||
case int intValue:
|
||||
return JsonValue.Create(intValue);
|
||||
case long longValue:
|
||||
return JsonValue.Create(longValue);
|
||||
case double doubleValue:
|
||||
return JsonValue.Create(doubleValue);
|
||||
case float floatValue:
|
||||
return JsonValue.Create(floatValue);
|
||||
case decimal decimalValue:
|
||||
return JsonValue.Create(decimalValue);
|
||||
case DateTime dateTime:
|
||||
return JsonValue.Create(dateTime.ToString("O"));
|
||||
case IDictionary dictionary:
|
||||
{
|
||||
var obj = new JsonObject();
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
var key = entry.Key?.ToString() ?? string.Empty;
|
||||
obj[key] = ConvertToJsonNode(entry.Value);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
case IEnumerable enumerable:
|
||||
{
|
||||
if (value is string)
|
||||
{
|
||||
return JsonValue.Create(value.ToString());
|
||||
}
|
||||
|
||||
var array = new JsonArray();
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
array.Add(ConvertToJsonNode(item));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
default:
|
||||
return JsonValue.Create(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EvalTestSuite
|
||||
{
|
||||
[YamlMember(Alias = "test_cases")]
|
||||
public List<EvalTestCase> TestCases { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class EvalTestCase
|
||||
{
|
||||
[YamlMember(Alias = "name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember(Alias = "condition")]
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember(Alias = "expected")]
|
||||
public bool Expected { get; set; }
|
||||
|
||||
[YamlMember(Alias = "context")]
|
||||
public EvalContextOverrides? Context { get; set; }
|
||||
}
|
||||
|
||||
private sealed class EvalContextOverrides
|
||||
{
|
||||
[YamlMember(Alias = "action")]
|
||||
public string? Action { get; set; }
|
||||
|
||||
[YamlMember(Alias = "suboperation")]
|
||||
public string? Suboperation { get; set; }
|
||||
|
||||
[YamlMember(Alias = "request_action")]
|
||||
public string? RequestAction { get; set; }
|
||||
|
||||
[YamlMember(Alias = "data_action")]
|
||||
public string? DataAction { get; set; }
|
||||
|
||||
[YamlMember(Alias = "principal_id")]
|
||||
public string? PrincipalId { get; set; }
|
||||
|
||||
[YamlMember(Alias = "principal_type")]
|
||||
public string? PrincipalType { get; set; }
|
||||
|
||||
[YamlMember(Alias = "resource_id")]
|
||||
public string? ResourceId { get; set; }
|
||||
|
||||
[YamlMember(Alias = "resource_type")]
|
||||
public string? ResourceType { get; set; }
|
||||
|
||||
[YamlMember(Alias = "resource_scope")]
|
||||
public string? ResourceScope { get; set; }
|
||||
|
||||
[YamlMember(Alias = "request_attributes")]
|
||||
public object? RequestAttributes { get; set; }
|
||||
|
||||
[YamlMember(Alias = "resource_attributes")]
|
||||
public object? ResourceAttributes { get; set; }
|
||||
|
||||
[YamlMember(Alias = "principal_custom_security_attributes")]
|
||||
public object? PrincipalCustomSecurityAttributes { get; set; }
|
||||
|
||||
[YamlMember(Alias = "environment")]
|
||||
public EvalEnvironmentOverrides? Environment { get; set; }
|
||||
}
|
||||
|
||||
private sealed class EvalEnvironmentOverrides
|
||||
{
|
||||
[YamlMember(Alias = "is_private_link")]
|
||||
public bool? IsPrivateLink { get; set; }
|
||||
|
||||
[YamlMember(Alias = "private_endpoint")]
|
||||
public string? PrivateEndpoint { get; set; }
|
||||
|
||||
[YamlMember(Alias = "subnet")]
|
||||
public string? Subnet { get; set; }
|
||||
|
||||
[YamlMember(Alias = "utc_now")]
|
||||
public string? UtcNow { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,14 @@
|
||||
|
||||
<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>
|
||||
153
bindings/csharp/Regorus/AliasRegistry.cs
Normal file
153
bindings/csharp/Regorus/AliasRegistry.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using Regorus.Internal;
|
||||
|
||||
#nullable enable
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages Azure Policy alias definitions used for resource normalization
|
||||
/// and policy compilation.
|
||||
/// </summary>
|
||||
public unsafe sealed class AliasRegistry : SafeHandleWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an empty alias registry.
|
||||
/// </summary>
|
||||
public AliasRegistry()
|
||||
: base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load control-plane alias data (array of ProviderAliases) from a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json)</param>
|
||||
public void LoadJson(string json)
|
||||
{
|
||||
Utf8Marshaller.WithUtf8(json, jsonPtr =>
|
||||
{
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
CheckAndDropResult(API.regorus_alias_registry_load_json(
|
||||
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a data-plane policy manifest from a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">JSON object containing a DataPolicyManifest</param>
|
||||
public void LoadManifest(string json)
|
||||
{
|
||||
Utf8Marshaller.WithUtf8(json, jsonPtr =>
|
||||
{
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
CheckAndDropResult(API.regorus_alias_registry_load_manifest(
|
||||
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of resource types loaded in the registry.
|
||||
/// </summary>
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetIntResult(
|
||||
API.regorus_alias_registry_len((RegorusAliasRegistry*)regPtr));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope
|
||||
/// expected by a compiled Azure Policy program.
|
||||
/// </summary>
|
||||
/// <param name="resourceJson">Raw ARM resource JSON</param>
|
||||
/// <param name="apiVersion">API version string (e.g. "2023-01-01"), or null to use default alias paths</param>
|
||||
/// <param name="contextJson">Additional context JSON object (pass "{}" if none)</param>
|
||||
/// <param name="parametersJson">Policy parameter values JSON (pass "{}" if none)</param>
|
||||
/// <returns>JSON string: { "resource": <normalized>, "context": <context>, "parameters": <params> }</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
bindings/csharp/Regorus/CacheConfig.cs
Normal file
39
bindings/csharp/Regorus/CacheConfig.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Global configuration for compiled pattern caches used by regex and glob builtins.
|
||||
/// </summary>
|
||||
public readonly struct CacheConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CacheConfig"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="regex">Maximum cached compiled regex patterns (default 256, 0 = disabled).</param>
|
||||
/// <param name="glob">Maximum cached compiled glob matchers (default 128, 0 = disabled).</param>
|
||||
public CacheConfig(nuint regex, nuint glob)
|
||||
{
|
||||
Regex = regex;
|
||||
Glob = glob;
|
||||
}
|
||||
|
||||
/// <summary>Maximum cached compiled regex patterns (default 256).</summary>
|
||||
public nuint Regex { get; }
|
||||
|
||||
/// <summary>Maximum cached compiled glob matchers (default 128).</summary>
|
||||
public nuint Glob { get; }
|
||||
|
||||
internal Regorus.Internal.RegorusCacheConfig ToNative()
|
||||
{
|
||||
return new Regorus.Internal.RegorusCacheConfig
|
||||
{
|
||||
regex = Regex,
|
||||
glob = Glob,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,17 @@ 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))
|
||||
{
|
||||
@@ -82,6 +93,24 @@ 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 =>
|
||||
|
||||
@@ -428,6 +428,18 @@ 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
|
||||
@@ -446,6 +458,22 @@ 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>
|
||||
@@ -492,6 +520,16 @@ namespace Regorus.Internal
|
||||
|
||||
#endregion
|
||||
|
||||
#region RBAC Methods
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_rbac_engine_eval_condition", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_rbac_engine_eval_condition(byte* condition, byte* context_json);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Target Registry Methods
|
||||
|
||||
/// <summary>
|
||||
@@ -631,6 +669,55 @@ namespace Regorus.Internal
|
||||
internal static extern RegorusResult regorus_effect_schema_clear();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Alias Registry Methods
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty AliasRegistry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusAliasRegistry* regorus_alias_registry_new();
|
||||
|
||||
/// <summary>
|
||||
/// Drop an AliasRegistry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry);
|
||||
|
||||
/// <summary>
|
||||
/// Load control-plane alias data (array of ProviderAliases) into the registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json);
|
||||
|
||||
/// <summary>
|
||||
/// Load a data-plane policy manifest into the registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json);
|
||||
|
||||
/// <summary>
|
||||
/// Return the number of resource types loaded in the alias registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_len(RegorusAliasRegistry* registry);
|
||||
|
||||
/// <summary>
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
|
||||
/// Returns a JSON string.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_normalize_and_wrap", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_normalize_and_wrap(
|
||||
RegorusAliasRegistry* registry, byte* resource_json, byte* api_version, byte* context_json, byte* parameters_json);
|
||||
|
||||
/// <summary>
|
||||
/// Denormalize a previously-normalized resource JSON back to ARM format.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_denormalize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_denormalize(
|
||||
RegorusAliasRegistry* registry, byte* normalized_json, byte* api_version);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Native Structures
|
||||
@@ -762,6 +849,27 @@ 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>
|
||||
@@ -815,5 +923,13 @@ namespace Regorus.Internal
|
||||
public byte* content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for AliasRegistry.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe partial struct RegorusAliasRegistry
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
53
bindings/csharp/Regorus/PolicyLengthConfig.cs
Normal file
53
bindings/csharp/Regorus/PolicyLengthConfig.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy source length limits enforced when loading policy files.
|
||||
/// </summary>
|
||||
public readonly struct PolicyLengthConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolicyLengthConfig"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="maxCol">Maximum column width per line. Must be non-zero.</param>
|
||||
/// <param name="maxFileBytes">Maximum policy file size in bytes. Must be non-zero.</param>
|
||||
/// <param name="maxLines">Maximum number of lines per policy file. Must be non-zero.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when any parameter is zero.</exception>
|
||||
public PolicyLengthConfig(uint maxCol, nuint maxFileBytes, nuint maxLines)
|
||||
{
|
||||
if (maxCol == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxCol), "Must be non-zero.");
|
||||
if (maxFileBytes == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxFileBytes), "Must be non-zero.");
|
||||
if (maxLines == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxLines), "Must be non-zero.");
|
||||
|
||||
MaxCol = maxCol;
|
||||
MaxFileBytes = maxFileBytes;
|
||||
MaxLines = maxLines;
|
||||
}
|
||||
|
||||
/// <summary>Maximum column width per line (default: 1024).</summary>
|
||||
public uint MaxCol { get; }
|
||||
|
||||
/// <summary>Maximum policy file size in bytes (default: 1 MiB).</summary>
|
||||
public nuint MaxFileBytes { get; }
|
||||
|
||||
/// <summary>Maximum number of lines per policy file (default: 20000).</summary>
|
||||
public nuint MaxLines { get; }
|
||||
|
||||
internal Regorus.Internal.RegorusPolicyLengthConfig ToNative()
|
||||
{
|
||||
return new Regorus.Internal.RegorusPolicyLengthConfig
|
||||
{
|
||||
max_col = MaxCol,
|
||||
max_file_bytes = MaxFileBytes,
|
||||
max_lines = MaxLines,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
45
bindings/csharp/Regorus/RbacEngine.cs
Normal file
45
bindings/csharp/Regorus/RbacEngine.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using Regorus.Internal;
|
||||
|
||||
#nullable enable
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides helpers for evaluating Azure RBAC condition expressions.
|
||||
/// </summary>
|
||||
public static unsafe class RbacEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
|
||||
/// </summary>
|
||||
/// <param name="condition">Azure RBAC condition expression.</param>
|
||||
/// <param name="contextJson">JSON encoded EvaluationContext.</param>
|
||||
/// <returns>True if the condition evaluates to true; otherwise false.</returns>
|
||||
/// <exception cref="Exception">Thrown when evaluation fails.</exception>
|
||||
public static bool EvaluateCondition(string condition, string contextJson)
|
||||
{
|
||||
if (condition is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(condition));
|
||||
}
|
||||
|
||||
if (contextJson is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contextJson));
|
||||
}
|
||||
|
||||
return Utf8Marshaller.WithUtf8(condition, conditionPtr =>
|
||||
Utf8Marshaller.WithUtf8(contextJson, contextPtr =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var result = Internal.API.regorus_rbac_engine_eval_condition((byte*)conditionPtr, (byte*)contextPtr);
|
||||
return ResultHelpers.GetBoolResult(result);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,4 +183,52 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,13 @@ var w = new Stopwatch();
|
||||
|
||||
w.Restart();
|
||||
|
||||
// Configure the global pattern caches.
|
||||
Regorus.Engine.SetCacheConfig(new Regorus.CacheConfig(regex: 256, glob: 128));
|
||||
|
||||
var engine = new Regorus.Engine();
|
||||
engine.SetRegoV0(true);
|
||||
// Raise the default col limit to 2000
|
||||
engine.SetPolicyLengthConfig(new Regorus.PolicyLengthConfig(maxCol: 2000, maxFileBytes: 1048576, maxLines: 20000));
|
||||
|
||||
w.Stop();
|
||||
var newEngineTicks = w.ElapsedTicks;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.NoTargets": "3.7.56"
|
||||
},
|
||||
"sdk": {
|
||||
"allowPrerelease": false,
|
||||
"version": "8.0.412",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.NoTargets": "3.7.134"
|
||||
},
|
||||
"sdk": {
|
||||
"allowPrerelease": false,
|
||||
"version": "8.0.412",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
570
bindings/ffi/Cargo.lock
generated
570
bindings/ffi/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -32,8 +32,10 @@ default = [
|
||||
"coverage",
|
||||
"allocator-memory-limits",
|
||||
"rvm",
|
||||
"rbac",
|
||||
"regorus/arc",
|
||||
"regorus/full-opa",
|
||||
"cache",
|
||||
"contention_checks",
|
||||
]
|
||||
ast = ["regorus/ast"]
|
||||
@@ -43,6 +45,8 @@ 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]
|
||||
|
||||
479
bindings/ffi/src/alias_registry.rs
Normal file
479
bindings/ffi/src/alias_registry.rs
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! FFI bindings for `AliasRegistry` – Azure Policy alias catalog management.
|
||||
|
||||
#![cfg(feature = "azure_policy")]
|
||||
|
||||
use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus};
|
||||
use crate::panic_guard::with_unwind_guard;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use anyhow::Result;
|
||||
use core::ffi::c_char;
|
||||
use core::ptr;
|
||||
|
||||
use regorus::languages::azure_policy::aliases::AliasRegistry;
|
||||
|
||||
/// Opaque wrapper for `AliasRegistry`.
|
||||
pub struct RegorusAliasRegistry {
|
||||
registry: AliasRegistry,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create a new, empty `AliasRegistry`.
|
||||
///
|
||||
/// The caller must eventually call `regorus_alias_registry_drop` to free the handle.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry {
|
||||
let wrapper = RegorusAliasRegistry {
|
||||
registry: AliasRegistry::new(),
|
||||
};
|
||||
Box::into_raw(Box::new(wrapper))
|
||||
}
|
||||
|
||||
/// Drop a `RegorusAliasRegistry`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) {
|
||||
if let Ok(r) = to_ref(registry) {
|
||||
unsafe {
|
||||
let _ = Box::from_raw(ptr::from_mut(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load control-plane alias data (array of `ProviderAliases`) into the registry.
|
||||
///
|
||||
/// `json` must be a valid null-terminated UTF-8 string containing the JSON
|
||||
/// array returned by `Get-AzPolicyAlias` or the static
|
||||
/// `ResourceTypesAndAliases.json` file.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_load_json(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<()> {
|
||||
let json_str = from_c_str(json)?;
|
||||
to_ref(registry)?.registry.load_from_json(&json_str)?;
|
||||
Ok(())
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(()) => RegorusResult::ok_void(),
|
||||
Err(e) => RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidDataFormat,
|
||||
format!("Failed to load alias catalog: {e}"),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a data-plane policy manifest into the registry.
|
||||
///
|
||||
/// `json` must be a valid null-terminated UTF-8 string containing a single
|
||||
/// `DataPolicyManifest` JSON object.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_load_manifest(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<()> {
|
||||
let json_str = from_c_str(json)?;
|
||||
to_ref(registry)?
|
||||
.registry
|
||||
.load_data_policy_manifest_json(&json_str)?;
|
||||
Ok(())
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(()) => RegorusResult::ok_void(),
|
||||
Err(e) => RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidDataFormat,
|
||||
format!("Failed to load data-plane manifest: {e}"),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return the number of resource types loaded in the alias registry.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<i64> {
|
||||
let len = to_ref(registry)?.registry.len();
|
||||
Ok(len as i64)
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(n) => RegorusResult::ok_int(n),
|
||||
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalize / Denormalize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
|
||||
///
|
||||
/// Returns a JSON string:
|
||||
/// `{ "resource": <normalized>, "context": <context>, "parameters": <params> }`.
|
||||
///
|
||||
/// * `resource_json` – raw ARM resource JSON
|
||||
/// * `api_version` – API version string (e.g. `"2023-01-01"`), or null to use
|
||||
/// the default alias paths
|
||||
/// * `context_json` – JSON object for additional context (pass `"{}"` if none)
|
||||
/// * `parameters_json` – JSON object of policy parameter values (pass `"{}"` if none)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
resource_json: *const c_char,
|
||||
api_version: *const c_char,
|
||||
context_json: *const c_char,
|
||||
parameters_json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<String> {
|
||||
let resource_str = from_c_str(resource_json)?;
|
||||
let api_ver = if api_version.is_null() {
|
||||
None
|
||||
} else {
|
||||
let s = from_c_str(api_version)?;
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
};
|
||||
let context_str = from_c_str(context_json)?;
|
||||
let params_str = from_c_str(parameters_json)?;
|
||||
|
||||
let resource = regorus::Value::from_json_str(&resource_str)?;
|
||||
let context = regorus::Value::from_json_str(&context_str)?;
|
||||
let params = regorus::Value::from_json_str(¶ms_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);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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.
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -491,6 +492,37 @@ 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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod alias_registry;
|
||||
mod allocator;
|
||||
mod common;
|
||||
mod compile;
|
||||
@@ -14,6 +15,8 @@ mod engine;
|
||||
mod limits;
|
||||
mod lock;
|
||||
mod panic_guard;
|
||||
#[cfg(feature = "rbac")]
|
||||
mod rbac;
|
||||
#[cfg(feature = "rvm")]
|
||||
pub(crate) mod rvm;
|
||||
mod schema_registry;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::common::{to_regorus_result, RegorusResult, RegorusStatus};
|
||||
use alloc::format;
|
||||
use anyhow::{anyhow, Result};
|
||||
use core::num::NonZeroU32;
|
||||
use core::num::{NonZeroU32, NonZeroUsize};
|
||||
use core::time::Duration;
|
||||
use regorus::utils::limits::{self, ExecutionTimerConfig};
|
||||
|
||||
@@ -158,6 +158,31 @@ 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,
|
||||
@@ -174,6 +199,40 @@ 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::{
|
||||
|
||||
59
bindings/ffi/src/rbac.rs
Normal file
59
bindings/ffi/src/rbac.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::common::{from_c_str, RegorusResult, RegorusStatus};
|
||||
use crate::panic_guard::with_unwind_guard;
|
||||
use alloc::format;
|
||||
use core::ffi::c_char;
|
||||
|
||||
use regorus::languages::azure_rbac::ast::EvaluationContext;
|
||||
use regorus::languages::azure_rbac::interpreter::ConditionInterpreter;
|
||||
|
||||
#[no_mangle]
|
||||
/// Evaluate an Azure RBAC condition expression against a JSON evaluation context.
|
||||
///
|
||||
/// * `condition`: RBAC condition string.
|
||||
/// * `context_json`: JSON representation of EvaluationContext.
|
||||
pub extern "C" fn regorus_rbac_engine_eval_condition(
|
||||
condition: *const c_char,
|
||||
context_json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let condition = match from_c_str(condition) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidArgument,
|
||||
format!("{err}"),
|
||||
)
|
||||
}
|
||||
};
|
||||
let context_json = match from_c_str(context_json) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidArgument,
|
||||
format!("{err}"),
|
||||
)
|
||||
}
|
||||
};
|
||||
let context: EvaluationContext = match serde_json::from_str(&context_json) {
|
||||
Ok(context) => context,
|
||||
Err(err) => {
|
||||
return RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidDataFormat,
|
||||
format!("invalid context json: {err}"),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let interpreter = ConditionInterpreter::new(&context);
|
||||
match interpreter.evaluate_str(&condition) {
|
||||
Ok(result) => RegorusResult::ok_bool(result),
|
||||
Err(err) => RegorusResult::err_with_message(
|
||||
RegorusStatus::Error,
|
||||
format!("condition evaluation failed: {err}"),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -17,7 +17,15 @@ func main() {
|
||||
engine := regorus.NewEngine()
|
||||
defer engine.Close()
|
||||
|
||||
// Configure the global pattern caches.
|
||||
if err = regorus.SetCacheConfig(regorus.CacheConfig{Regex: 256, Glob: 128}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
engine.SetRegoV0(true)
|
||||
// Raise the default col limit to 2000
|
||||
engine.SetPolicyLengthConfig(regorus.PolicyLengthConfig{MaxCol: 2000, MaxFileBytes: 1048576, MaxLines: 20000})
|
||||
elapsed1 := time.Since(t)
|
||||
|
||||
|
||||
|
||||
@@ -214,3 +214,59 @@ func (e *Engine) TakePrints() (string, error) {
|
||||
|
||||
return C.GoString(result.output), nil
|
||||
}
|
||||
|
||||
type PolicyLengthConfig struct {
|
||||
MaxCol uint32
|
||||
MaxFileBytes uint
|
||||
MaxLines uint
|
||||
}
|
||||
|
||||
func (e *Engine) SetPolicyLengthConfig(config PolicyLengthConfig) error {
|
||||
c := C.RegorusPolicyLengthConfig{
|
||||
max_col: C.uint32_t(config.MaxCol),
|
||||
max_file_bytes: C.size_t(config.MaxFileBytes),
|
||||
max_lines: C.size_t(config.MaxLines),
|
||||
}
|
||||
result := C.regorus_engine_set_policy_length_config(e.e, c)
|
||||
defer C.regorus_result_drop(result)
|
||||
if result.status != C.Ok {
|
||||
return fmt.Errorf("%s", C.GoString(result.error_message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) ClearPolicyLengthConfig() error {
|
||||
result := C.regorus_engine_clear_policy_length_config(e.e)
|
||||
defer C.regorus_result_drop(result)
|
||||
if result.status != C.Ok {
|
||||
return fmt.Errorf("%s", C.GoString(result.error_message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Regex uint
|
||||
Glob uint
|
||||
}
|
||||
|
||||
func SetCacheConfig(config CacheConfig) error {
|
||||
c := C.RegorusCacheConfig{
|
||||
regex: C.size_t(config.Regex),
|
||||
glob: C.size_t(config.Glob),
|
||||
}
|
||||
result := C.regorus_set_cache_config(c)
|
||||
defer C.regorus_result_drop(result)
|
||||
if result.status != C.Ok {
|
||||
return fmt.Errorf("%s", C.GoString(result.error_message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearCache() error {
|
||||
result := C.regorus_clear_cache()
|
||||
defer C.regorus_result_drop(result)
|
||||
if result.status != C.Ok {
|
||||
return fmt.Errorf("%s", C.GoString(result.error_message))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
644
bindings/java/Cargo.lock
generated
644
bindings/java/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,12 +14,13 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
|
||||
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
|
||||
coverage = ["regorus/coverage"]
|
||||
ast = ["regorus/ast"]
|
||||
cache = ["regorus/cache"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
serde_json = "1.0.112"
|
||||
jni = "0.21.1"
|
||||
jni = "0.22.4"
|
||||
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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;
|
||||
@@ -9,6 +11,9 @@ 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",
|
||||
@@ -26,6 +31,9 @@ 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);
|
||||
|
||||
@@ -48,13 +48,13 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<version>2.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -76,7 +76,7 @@
|
||||
<plugin>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<version>3.1.0</version>
|
||||
<version>3.6.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<!-- Build a debug release for tests -->
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<version>3.5.5</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>
|
||||
@@ -108,7 +108,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<version>3.12.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
@@ -123,7 +123,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// 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::JNIEnv;
|
||||
use jni::{jni_str, Env, EnvUnowned, Outcome};
|
||||
|
||||
use regorus::languages::rego::compiler::Compiler;
|
||||
use regorus::rvm::program::{
|
||||
@@ -16,7 +18,7 @@ use std::sync::Arc;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jlong {
|
||||
let engine = Engine::new();
|
||||
@@ -25,7 +27,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) -> jlong {
|
||||
@@ -36,7 +38,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClone(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
enable: bool,
|
||||
@@ -50,7 +52,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetRegoV0(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
@@ -58,9 +60,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 = env.get_string(&path)?.into();
|
||||
let rego: String = env.get_string(®o)?.into();
|
||||
let pkg = env.new_string(engine.add_policy(path, rego)?)?;
|
||||
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)?)?;
|
||||
Ok(pkg.into_raw())
|
||||
});
|
||||
|
||||
@@ -72,15 +74,15 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicy(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.get_string(&path)?.into();
|
||||
let pkg = env.new_string(engine.add_policy_from_file(path)?)?;
|
||||
let path: String = path.try_to_string(env)?;
|
||||
let pkg = JString::new(env, engine.add_policy_from_file(path)?)?;
|
||||
Ok(pkg.into_raw())
|
||||
});
|
||||
|
||||
@@ -92,14 +94,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddPolicyFromFile
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPackages(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(serde_json::to_string_pretty(&packages)?)?;
|
||||
let packages_json = JString::new(env, serde_json::to_string_pretty(&packages)?)?;
|
||||
Ok(packages_json.into_raw())
|
||||
});
|
||||
|
||||
@@ -111,14 +113,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPackages(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPolicies(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(&policies)?;
|
||||
let policies_json = JString::new(env, &policies)?;
|
||||
Ok(policies_json.into_raw())
|
||||
});
|
||||
|
||||
@@ -130,7 +132,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeGetPolicies(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearData(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) {
|
||||
@@ -143,14 +145,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearData(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJson(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
data: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let data: String = env.get_string(&data)?.into();
|
||||
let data: String = data.try_to_string(env)?;
|
||||
engine.add_data_json(&data)?;
|
||||
Ok(())
|
||||
});
|
||||
@@ -158,14 +160,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJson(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFile(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
let path: String = path.try_to_string(env)?;
|
||||
engine.add_data(Value::from_json_file(path)?)?;
|
||||
Ok(())
|
||||
});
|
||||
@@ -173,14 +175,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeAddDataJsonFromFi
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJson(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
input: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let input: String = env.get_string(&input)?.into();
|
||||
let input: String = input.try_to_string(env)?;
|
||||
engine.set_input_json(&input)?;
|
||||
Ok(())
|
||||
});
|
||||
@@ -188,14 +190,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJson(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromFile(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
path: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
|
||||
let path: String = env.get_string(&path)?.into();
|
||||
let path: String = path.try_to_string(env)?;
|
||||
engine.set_input(Value::from_json_file(path)?);
|
||||
Ok(())
|
||||
});
|
||||
@@ -203,16 +205,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetInputJsonFromF
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalQuery(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.get_string(&query)?.into();
|
||||
let query: String = query.try_to_string(env)?;
|
||||
let results = engine.eval_query(query, false)?;
|
||||
let output = env.new_string(serde_json::to_string(&results)?)?;
|
||||
let output = JString::new(env, serde_json::to_string(&results)?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -224,16 +226,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalQuery(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeEvalRule(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.get_string(&rule)?.into();
|
||||
let rule: String = rule.try_to_string(env)?;
|
||||
let value = engine.eval_rule(rule)?;
|
||||
let output = env.new_string(value.to_json_str()?)?;
|
||||
let output = JString::new(env, value.to_json_str()?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -246,7 +248,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
enable: bool,
|
||||
@@ -261,14 +263,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(serde_json::to_string_pretty(&report)?)?;
|
||||
let output = JString::new(env, serde_json::to_string_pretty(&report)?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -281,14 +283,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(&report)?;
|
||||
let output = JString::new(env, &report)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -301,7 +303,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) {
|
||||
@@ -314,7 +316,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeClearCoverageData
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetGatherPrints(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
b: bool,
|
||||
@@ -328,14 +330,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeSetGatherPrints(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeTakePrints(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(serde_json::to_string_pretty(&prints)?)?;
|
||||
let output = JString::new(env, serde_json::to_string_pretty(&prints)?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -348,14 +350,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_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 = env.new_string(&ast)?;
|
||||
let output = JString::new(env, &ast)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -365,9 +367,73 @@ 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: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
) {
|
||||
@@ -378,7 +444,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModules(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
data_json: JString,
|
||||
module_ids: jobjectArray,
|
||||
@@ -386,7 +452,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 = env.get_string(&data_json)?.into();
|
||||
let data_json: String = data_json.try_to_string(env)?;
|
||||
let data = Value::from_json_str(&data_json)?;
|
||||
|
||||
let ids = get_string_array(env, module_ids)?;
|
||||
@@ -422,7 +488,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModul
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngine(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
engine_ptr: jlong,
|
||||
entry_points: jobjectArray,
|
||||
@@ -447,7 +513,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngin
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
program_ptr: jlong,
|
||||
) -> jstring {
|
||||
@@ -455,7 +521,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 = env.new_string(&listing)?;
|
||||
let output = JString::new(env, &listing)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -467,7 +533,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Program_nativeSerializeBinary(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
program_ptr: jlong,
|
||||
) -> jbyteArray {
|
||||
@@ -491,7 +557,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: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
data: jbyteArray,
|
||||
is_partial: jbooleanArray,
|
||||
@@ -501,7 +567,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(data) };
|
||||
let data = unsafe { JByteArray::from_raw(env, data) };
|
||||
let bytes = env.convert_byte_array(&data)?;
|
||||
let (program, partial) =
|
||||
match RvmProgram::deserialize_binary(&bytes).map_err(|e| anyhow::anyhow!(e))? {
|
||||
@@ -510,11 +576,15 @@ pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeseriali
|
||||
};
|
||||
|
||||
if !is_partial.is_null() {
|
||||
let is_partial = unsafe { JBooleanArray::from_raw(is_partial) };
|
||||
let len = env.get_array_length(&is_partial)?;
|
||||
let is_partial = unsafe { JBooleanArray::from_raw(env, is_partial) };
|
||||
let len = is_partial.len(env)?;
|
||||
if len > 0 {
|
||||
let value: [jboolean; 1] = [if partial { 1 } else { 0 }];
|
||||
env.set_boolean_array_region(&is_partial, 0, &value)?;
|
||||
let value: [jboolean; 1] = [if partial {
|
||||
jni::sys::JNI_TRUE
|
||||
} else {
|
||||
jni::sys::JNI_FALSE
|
||||
}];
|
||||
is_partial.set_region(env, 0, &value)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +596,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: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
program_ptr: jlong,
|
||||
) {
|
||||
@@ -537,7 +607,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeDrop(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jlong {
|
||||
let vm = RegoVM::new();
|
||||
@@ -546,7 +616,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
program_ptr: jlong,
|
||||
@@ -561,14 +631,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
data_json: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
|
||||
let data_json: String = env.get_string(&data_json)?.into();
|
||||
let data_json: String = data_json.try_to_string(env)?;
|
||||
let data = Value::from_json_str(&data_json)?;
|
||||
vm.set_data(data)?;
|
||||
Ok(())
|
||||
@@ -577,14 +647,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
input_json: JString,
|
||||
) {
|
||||
let _ = throw_err(env, |env| {
|
||||
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
|
||||
let input_json: String = env.get_string(&input_json)?.into();
|
||||
let input_json: String = input_json.try_to_string(env)?;
|
||||
let input = Value::from_json_str(&input_json)?;
|
||||
vm.set_input(input);
|
||||
Ok(())
|
||||
@@ -593,7 +663,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
mode: u8,
|
||||
@@ -612,14 +682,14 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
) -> jstring {
|
||||
let res = throw_err(env, |env| {
|
||||
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
|
||||
let result = vm.execute()?;
|
||||
let output = env.new_string(result.to_json_str()?)?;
|
||||
let output = JString::new(env, result.to_json_str()?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -631,16 +701,16 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
entry_point: JString,
|
||||
) -> jstring {
|
||||
let res = throw_err(env, |env| {
|
||||
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
|
||||
let entry_point: String = env.get_string(&entry_point)?.into();
|
||||
let entry_point: String = entry_point.try_to_string(env)?;
|
||||
let result = vm.execute_entry_point_by_name(&entry_point)?;
|
||||
let output = env.new_string(result.to_json_str()?)?;
|
||||
let output = JString::new(env, result.to_json_str()?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -652,7 +722,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
resume_json: JString,
|
||||
@@ -661,13 +731,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 = env.get_string(&resume_json)?.into();
|
||||
let resume_json: String = resume_json.try_to_string(env)?;
|
||||
Some(Value::from_json_str(&resume_json)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result = vm.resume(value)?;
|
||||
let output = env.new_string(result.to_json_str()?)?;
|
||||
let output = JString::new(env, result.to_json_str()?)?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -679,13 +749,13 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState(
|
||||
env: JNIEnv,
|
||||
env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
) -> jstring {
|
||||
let res = throw_err(env, |env| {
|
||||
let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) };
|
||||
let output = env.new_string(format!("{:?}", vm.execution_state()))?;
|
||||
let output = JString::new(env, format!("{:?}", vm.execution_state()))?;
|
||||
Ok(output.into_raw())
|
||||
});
|
||||
|
||||
@@ -697,7 +767,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState(
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
vm_ptr: jlong,
|
||||
) {
|
||||
@@ -706,27 +776,57 @@ pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop(
|
||||
}
|
||||
}
|
||||
|
||||
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())?;
|
||||
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(())
|
||||
});
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string_array(env: &mut JNIEnv, array: jobjectArray) -> Result<Vec<String>> {
|
||||
fn get_string_array(env: &mut Env, array: jobjectArray) -> Result<Vec<String>> {
|
||||
if array.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let array = unsafe { JObjectArray::from_raw(array) };
|
||||
let len = env.get_array_length(&array)?;
|
||||
let mut values = Vec::with_capacity(len as usize);
|
||||
let array = unsafe { JObjectArray::<JObject>::from_raw(env, array) };
|
||||
let len = array.len(env)?;
|
||||
let mut values = Vec::with_capacity(len);
|
||||
for i in 0..len {
|
||||
let obj = env.get_object_array_element(&array, i)?;
|
||||
let jstr = JString::from(obj);
|
||||
let value: String = env.get_string(&jstr)?.into();
|
||||
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)?;
|
||||
values.push(value);
|
||||
}
|
||||
Ok(values)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ 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
|
||||
@@ -259,6 +261,22 @@ 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
519
bindings/python/Cargo.lock
generated
519
bindings/python/Cargo.lock
generated
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
@@ -25,6 +25,12 @@ 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"
|
||||
@@ -36,9 +42,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -46,22 +52,6 @@ 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"
|
||||
@@ -79,9 +69,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -101,9 +91,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -113,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -128,10 +118,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -150,12 +151,30 @@ 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"
|
||||
@@ -182,6 +201,18 @@ 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"
|
||||
@@ -190,9 +221,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.14.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
@@ -207,15 +238,27 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
|
||||
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
|
||||
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"
|
||||
@@ -242,9 +285,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"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]]
|
||||
@@ -259,11 +318,25 @@ 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"
|
||||
@@ -376,6 +449,12 @@ 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"
|
||||
@@ -404,37 +483,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -442,27 +512,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
|
||||
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
|
||||
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",
|
||||
]
|
||||
|
||||
@@ -473,10 +544,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -500,19 +577,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
@@ -604,15 +678,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "5.1.0"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d"
|
||||
checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
@@ -672,9 +746,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
@@ -686,12 +772,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -705,38 +792,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.24.2"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
|
||||
checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1"
|
||||
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.24.2"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
|
||||
checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.24.2"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
|
||||
checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@@ -744,9 +826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.24.2"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
|
||||
checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@@ -756,9 +838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.24.2"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
|
||||
checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -769,9 +851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -783,33 +865,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
@@ -842,13 +918,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
|
||||
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"fluent-uri",
|
||||
"once_cell",
|
||||
"getrandom 0.3.4",
|
||||
"hashbrown 0.16.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
@@ -856,9 +933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -868,9 +945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -879,16 +956,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"data-encoding",
|
||||
@@ -897,9 +973,12 @@ dependencies = [
|
||||
"ipnet",
|
||||
"jsonschema",
|
||||
"lazy_static",
|
||||
"lru",
|
||||
"msvc_spectre_libs",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"postcard",
|
||||
"rand",
|
||||
"regex",
|
||||
"regorus-mimalloc",
|
||||
@@ -946,9 +1025,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -1038,9 +1117,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
@@ -1050,9 +1129,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1072,9 +1151,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
@@ -1107,16 +1186,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.2.4"
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
@@ -1124,12 +1209,6 @@ 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"
|
||||
@@ -1150,14 +1229,12 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.20.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1167,7 +1244,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"uuid",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
@@ -1193,10 +1269,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
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 = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1207,9 +1292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1217,9 +1302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1230,13 +1315,47 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
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"
|
||||
@@ -1301,6 +1420,88 @@ 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"
|
||||
@@ -1333,18 +1534,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1407,6 +1608,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -15,14 +15,15 @@ keywords = ["interpreter", "opa", "policy-as-code", "rego"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
|
||||
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
|
||||
ast = ["regorus/ast"]
|
||||
cache = ["regorus/cache"]
|
||||
coverage = ["regorus/coverage"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
ordered-float = "5.0.0"
|
||||
pyo3 = { version = "0.24.1", features = ["abi3-py310", "anyhow", "extension-module"] }
|
||||
ordered-float = "5.3.0"
|
||||
pyo3 = { version = "0.28.2", features = ["abi3-py310", "anyhow", "extension-module"] }
|
||||
regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] }
|
||||
serde_json = "1.0.140"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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::*;
|
||||
@@ -43,7 +44,7 @@ impl Default for Engine {
|
||||
|
||||
fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
// dicts
|
||||
Ok(if let Ok(dict) = ob.downcast::<PyDict>() {
|
||||
Ok(if let Ok(dict) = ob.cast::<PyDict>() {
|
||||
let mut map = BTreeMap::new();
|
||||
for (k, v) in dict {
|
||||
map.insert(from(&k)?, from(&v)?);
|
||||
@@ -51,7 +52,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
map.into()
|
||||
}
|
||||
// set
|
||||
else if let Ok(pset) = ob.downcast::<PySet>() {
|
||||
else if let Ok(pset) = ob.cast::<PySet>() {
|
||||
let mut set = BTreeSet::new();
|
||||
for v in pset {
|
||||
set.insert(from(&v)?);
|
||||
@@ -59,7 +60,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
set.into()
|
||||
}
|
||||
// frozen set
|
||||
else if let Ok(pfset) = ob.downcast::<PyFrozenSet>() {
|
||||
else if let Ok(pfset) = ob.cast::<PyFrozenSet>() {
|
||||
//
|
||||
let mut set = BTreeSet::new();
|
||||
for v in pfset {
|
||||
@@ -68,13 +69,13 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
set.into()
|
||||
}
|
||||
// lists and tuples
|
||||
else if let Ok(plist) = ob.downcast::<PyList>() {
|
||||
else if let Ok(plist) = ob.cast::<PyList>() {
|
||||
let mut array = Vec::new();
|
||||
for v in plist {
|
||||
array.push(from(&v)?);
|
||||
}
|
||||
array.into()
|
||||
} else if let Ok(ptuple) = ob.downcast::<PyTuple>() {
|
||||
} else if let Ok(ptuple) = ob.cast::<PyTuple>() {
|
||||
let mut array = Vec::new();
|
||||
for v in ptuple {
|
||||
array.push(from(&v)?);
|
||||
@@ -85,6 +86,10 @@ 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()
|
||||
@@ -93,16 +98,12 @@ 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.downcast::<PyNone>().is_ok() {
|
||||
else if ob.cast::<PyNone>().is_ok() {
|
||||
Value::Null
|
||||
}
|
||||
// Anything that is a sequence
|
||||
else if let Ok(pseq) = ob.downcast::<PySequence>() {
|
||||
else if let Ok(pseq) = ob.cast::<PySequence>() {
|
||||
let mut array = Vec::new();
|
||||
for i in 0..pseq.len()? {
|
||||
array.push(from(&pseq.get_item(i)?)?);
|
||||
@@ -110,7 +111,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
array.into()
|
||||
}
|
||||
// Anything that is a map
|
||||
else if let Ok(pmap) = ob.downcast::<PyMapping>() {
|
||||
else if let Ok(pmap) = ob.cast::<PyMapping>() {
|
||||
let mut map = BTreeMap::new();
|
||||
let keys = pmap.keys()?;
|
||||
let values = pmap.values()?;
|
||||
@@ -127,7 +128,7 @@ fn from(ob: &Bound<'_, PyAny>) -> Result<Value, PyErr> {
|
||||
})
|
||||
}
|
||||
|
||||
fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
|
||||
fn to(mut v: Value, py: Python<'_>) -> Result<Py<PyAny>> {
|
||||
let obj = match v {
|
||||
Value::Null => None::<u64>.into_bound_py_any(py),
|
||||
|
||||
@@ -138,12 +139,17 @@ fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
|
||||
Value::String(s) => s.into_bound_py_any(py),
|
||||
|
||||
Value::Number(_) => {
|
||||
if let Ok(f) = v.as_f64() {
|
||||
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() {
|
||||
f.into_bound_py_any(py)
|
||||
} else if let Ok(u) = v.as_u64() {
|
||||
u.into_bound_py_any(py)
|
||||
} else {
|
||||
v.as_i64()?.into_bound_py_any(py)
|
||||
// fallback
|
||||
v.as_f64()?.into_bound_py_any(py)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +297,7 @@ impl Engine {
|
||||
/// Evaluate query.
|
||||
///
|
||||
/// * `query`: Rego expression to be evaluate.
|
||||
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<PyObject> {
|
||||
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<Py<PyAny>> {
|
||||
let results = self.engine.eval_query(query, false)?;
|
||||
|
||||
let rlist = PyList::empty(py);
|
||||
@@ -332,7 +338,7 @@ impl Engine {
|
||||
/// Evaluate rule.
|
||||
///
|
||||
/// * `rule`: Full path to the rule.
|
||||
pub fn eval_rule(&mut self, rule: String, py: Python<'_>) -> Result<PyObject> {
|
||||
pub fn eval_rule(&mut self, rule: String, py: Python<'_>) -> Result<Py<PyAny>> {
|
||||
to(self.engine.eval_rule(rule)?, py)
|
||||
}
|
||||
|
||||
@@ -344,6 +350,78 @@ 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.
|
||||
@@ -544,10 +622,33 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
# 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')
|
||||
@@ -163,3 +167,254 @@ 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()
|
||||
|
||||
392
bindings/ruby/Cargo.lock
generated
392
bindings/ruby/Cargo.lock
generated
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
@@ -25,6 +25,12 @@ 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"
|
||||
@@ -36,9 +42,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -46,12 +52,6 @@ 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"
|
||||
@@ -147,10 +147,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -186,6 +197,15 @@ 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"
|
||||
@@ -226,9 +246,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.14.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
@@ -243,15 +263,27 @@ checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
|
||||
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
|
||||
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"
|
||||
@@ -278,9 +310,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"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]]
|
||||
@@ -301,11 +349,31 @@ 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"
|
||||
@@ -412,6 +480,12 @@ 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"
|
||||
@@ -440,14 +514,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
@@ -476,27 +552,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
|
||||
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
|
||||
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",
|
||||
]
|
||||
|
||||
@@ -512,6 +589,12 @@ 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"
|
||||
@@ -550,10 +633,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "magnus"
|
||||
version = "0.7.1"
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
|
||||
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "magnus"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
||||
dependencies = [
|
||||
"magnus-macros",
|
||||
"rb-sys",
|
||||
@@ -563,9 +652,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "magnus-macros"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
|
||||
checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -578,20 +667,6 @@ version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "2.2.6"
|
||||
dependencies = [
|
||||
"mimalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc-sys"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -765,12 +840,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -798,33 +874,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "rb-sys"
|
||||
@@ -852,9 +922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rb-sys-env"
|
||||
version = "0.1.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
|
||||
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
@@ -887,13 +957,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
|
||||
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"fluent-uri",
|
||||
"once_cell",
|
||||
"getrandom 0.3.4",
|
||||
"hashbrown 0.16.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
@@ -901,9 +972,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -930,37 +1001,56 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
version = "0.5.0"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"data-encoding",
|
||||
"globset",
|
||||
"indexmap",
|
||||
"ipnet",
|
||||
"jsonschema",
|
||||
"lazy_static",
|
||||
"mimalloc",
|
||||
"lru",
|
||||
"msvc_spectre_libs",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"regex",
|
||||
"regorus-mimalloc",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"spin",
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regorus-mimalloc"
|
||||
version = "2.2.6"
|
||||
dependencies = [
|
||||
"regorus-mimalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regorus-mimalloc-sys"
|
||||
version = "2.2.6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regorusrb"
|
||||
version = "0.6.0"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"magnus",
|
||||
"regorus",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_magnus",
|
||||
]
|
||||
@@ -1046,9 +1136,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_magnus"
|
||||
version = "0.9.0"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b8b945a2dadb221f1c5490cfb411cab6c3821446b8eca50ee07e5a3893ec51"
|
||||
checksum = "8ff64c88ddd26acdcad5a501f18bcc339927b77b69f4a03bfaf2a6fc5ba2ac4b"
|
||||
dependencies = [
|
||||
"magnus",
|
||||
"serde",
|
||||
@@ -1092,6 +1182,12 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -1156,12 +1252,24 @@ 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"
|
||||
@@ -1188,14 +1296,12 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.19.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1205,7 +1311,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"uuid",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
@@ -1230,6 +1335,15 @@ 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"
|
||||
@@ -1275,6 +1389,40 @@ 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"
|
||||
@@ -1339,6 +1487,88 @@ 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"
|
||||
|
||||
@@ -7,10 +7,10 @@ gemspec
|
||||
|
||||
# These gems are required for local development and testing,
|
||||
# but won't be included in the published gem
|
||||
gem "minitest", "~> 5.25"
|
||||
gem "rake", "~> 13.2"
|
||||
gem "rake-compiler", "~> 1.2"
|
||||
gem "rake-compiler-dock", "~> 1.9"
|
||||
gem "rubocop", "~> 1.73", require: false
|
||||
gem "rubocop-minitest", "~> 0.37.1", require: false
|
||||
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-rake", "~> 0.7.1", require: false
|
||||
|
||||
@@ -7,25 +7,30 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.10.2)
|
||||
language_server-protocol (3.17.0.4)
|
||||
ast (2.4.3)
|
||||
drb (2.2.3)
|
||||
json (2.19.2)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
minitest (6.0.3)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.2)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prism (1.9.0)
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rake-compiler (1.2.9)
|
||||
rake (13.3.1)
|
||||
rake-compiler (1.3.1)
|
||||
rake
|
||||
rake-compiler-dock (1.9.1)
|
||||
rb_sys (0.9.111)
|
||||
rake-compiler-dock (= 1.9.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.73.2)
|
||||
rake-compiler-dock (1.11.0)
|
||||
rb_sys (0.9.125)
|
||||
json (>= 2)
|
||||
rake-compiler-dock (= 1.11.0)
|
||||
regexp_parser (2.11.3)
|
||||
rubocop (1.86.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -33,35 +38,36 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.37.1)
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-minitest (0.39.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rake (0.7.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (~> 5.25)
|
||||
rake (~> 13.2)
|
||||
rake-compiler (~> 1.2)
|
||||
rake-compiler-dock (~> 1.9)
|
||||
minitest (~> 6.0)
|
||||
rake (~> 13.3)
|
||||
rake-compiler (~> 1.3)
|
||||
rake-compiler-dock (~> 1.11)
|
||||
regorusrb!
|
||||
rubocop (~> 1.73)
|
||||
rubocop-minitest (~> 0.37.1)
|
||||
rubocop (~> 1.86)
|
||||
rubocop-minitest (~> 0.39.1)
|
||||
rubocop-rake (~> 0.7.1)
|
||||
|
||||
BUNDLED WITH
|
||||
|
||||
@@ -11,12 +11,14 @@ crate-type = ["cdylib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["ast", "coverage", "regorus/std", "regorus/full-opa"]
|
||||
default = ["ast", "cache", "coverage", "regorus/std", "regorus/full-opa", "regorus/allocator-memory-limits"]
|
||||
ast = ["regorus/ast"]
|
||||
cache = ["regorus/cache"]
|
||||
coverage = ["regorus/coverage"]
|
||||
|
||||
[dependencies]
|
||||
magnus = { version = "0.7.1" }
|
||||
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.9.0"
|
||||
serde_magnus = "0.11.0"
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
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 {
|
||||
@@ -55,15 +71,17 @@ impl Engine {
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add policy: {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}"),
|
||||
)
|
||||
})?;
|
||||
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}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine
|
||||
rb_self
|
||||
.engine
|
||||
.borrow_mut()
|
||||
.add_data(data_value)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to add data: {e}")))
|
||||
@@ -111,15 +129,16 @@ impl Engine {
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to get policies: {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}"),
|
||||
)
|
||||
})?;
|
||||
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}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.engine.borrow_mut().set_input(input_value);
|
||||
rb_self.engine.borrow_mut().set_input(input_value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -142,14 +161,14 @@ impl Engine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn eval_query(&self, query: String) -> Result<magnus::Value, Error> {
|
||||
let results = self
|
||||
fn eval_query(ruby: &Ruby, rb_self: &Self, query: String) -> Result<magnus::Value, Error> {
|
||||
let results = rb_self
|
||||
.engine
|
||||
.borrow_mut()
|
||||
.eval_query(query, false)
|
||||
.map_err(|e| Error::new(runtime_error(), format!("Failed to evaluate query: {e}")))?;
|
||||
|
||||
serde_magnus::serialize(&results).map_err(|e| {
|
||||
serde_magnus::serialize(ruby, &results).map_err(|e| {
|
||||
Error::new(
|
||||
runtime_error(),
|
||||
format!("Failed to serailzie query results: {e}"),
|
||||
@@ -177,15 +196,19 @@ impl Engine {
|
||||
})
|
||||
}
|
||||
|
||||
fn eval_rule(&self, query: String) -> Result<Option<magnus::Value>, Error> {
|
||||
fn eval_rule(
|
||||
ruby: &Ruby,
|
||||
rb_self: &Self,
|
||||
query: String,
|
||||
) -> Result<Option<magnus::Value>, Error> {
|
||||
let result =
|
||||
self.engine.borrow_mut().eval_rule(query).map_err(|e| {
|
||||
rb_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), // Convert undefined to Ruby's nil
|
||||
_ => serde_magnus::serialize(&result) // Serialize other results normally
|
||||
regorus::Value::Undefined => Ok(None),
|
||||
_ => serde_magnus::serialize(ruby, &result)
|
||||
.map(Some)
|
||||
.map_err(|e| {
|
||||
magnus::Error::new(
|
||||
@@ -280,6 +303,34 @@ 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
|
||||
@@ -361,7 +412,47 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -183,6 +183,16 @@ 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: [
|
||||
|
||||
528
bindings/wasm/Cargo.lock
generated
528
bindings/wasm/Cargo.lock
generated
@@ -25,6 +25,12 @@ 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"
|
||||
@@ -36,9 +42,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
@@ -57,22 +63,6 @@ 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"
|
||||
@@ -90,9 +80,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -112,9 +102,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -130,9 +120,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -145,10 +135,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.43"
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -167,12 +168,30 @@ 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"
|
||||
@@ -199,6 +218,18 @@ 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"
|
||||
@@ -207,9 +238,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.14.0"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
@@ -224,15 +255,27 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
|
||||
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
|
||||
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"
|
||||
@@ -254,26 +297,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -299,11 +341,27 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"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"
|
||||
@@ -316,11 +374,31 @@ 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"
|
||||
@@ -427,6 +505,12 @@ 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"
|
||||
@@ -455,56 +539,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
|
||||
checksum = "6f29616f6e19415398eb186964fb7cbbeef572c79bede3622a8277667924bbe3"
|
||||
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",
|
||||
]
|
||||
|
||||
@@ -515,10 +602,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -548,10 +641,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
@@ -663,9 +762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -728,15 +827,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
@@ -748,12 +853,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -767,9 +873,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -781,33 +887,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
@@ -840,13 +940,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.30.0"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
|
||||
checksum = "b8a618c14f8ba29d8193bb55e2bf13e4fb2b1115313ecb7ae94b43100c7ac7d5"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"fluent-uri",
|
||||
"once_cell",
|
||||
"getrandom 0.3.4",
|
||||
"hashbrown 0.16.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
@@ -854,9 +955,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -866,9 +967,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -877,16 +978,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"data-encoding",
|
||||
@@ -895,9 +995,12 @@ dependencies = [
|
||||
"ipnet",
|
||||
"jsonschema",
|
||||
"lazy_static",
|
||||
"lru",
|
||||
"msvc_spectre_libs",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"parking_lot",
|
||||
"postcard",
|
||||
"rand",
|
||||
"regex",
|
||||
"semver",
|
||||
@@ -916,8 +1019,10 @@ 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",
|
||||
@@ -932,9 +1037,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -967,6 +1072,17 @@ 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"
|
||||
@@ -1027,9 +1143,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -1039,9 +1155,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
@@ -1051,9 +1167,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1102,10 +1218,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
@@ -1113,12 +1241,6 @@ 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"
|
||||
@@ -1139,11 +1261,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.20.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
"rand",
|
||||
"wasm-bindgen",
|
||||
@@ -1156,7 +1278,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"uuid",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
@@ -1198,10 +1319,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
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 = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1212,23 +1342,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.58"
|
||||
version = "0.4.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
|
||||
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1236,9 +1362,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1249,18 +1375,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.58"
|
||||
version = "0.3.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d"
|
||||
checksum = "941c102b3f0c15b6d72a53205e09e6646aafcf2991e18412cc331dbac1806bc0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cast",
|
||||
@@ -1280,9 +1406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.58"
|
||||
version = "0.3.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2"
|
||||
checksum = "a26bd6570f39bb1440fd8f01b63461faaf2a3f6078a508e4e54efa99363108d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1291,18 +1417,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea"
|
||||
checksum = "1c29582b14d5bf030b02fa232b9b57faf2afc322d2c61964dd80bad02bf76207"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.85"
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"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]]
|
||||
@@ -1387,6 +1537,88 @@ 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"
|
||||
@@ -1419,18 +1651,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1493,6 +1725,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -32,9 +32,11 @@ default = [
|
||||
"regorus/time",
|
||||
"regorus/uuid",
|
||||
"regorus/urlquery",
|
||||
"regorus/yaml"
|
||||
"regorus/yaml",
|
||||
"cache"
|
||||
]
|
||||
ast = ["regorus/ast"]
|
||||
cache = ["regorus/cache"]
|
||||
coverage = ["regorus/coverage"]
|
||||
|
||||
[dependencies]
|
||||
@@ -42,15 +44,18 @@ 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.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"] }
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.40"
|
||||
wasm-bindgen-test = "0.3.67"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#![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,
|
||||
@@ -26,6 +27,42 @@ 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>,
|
||||
@@ -183,6 +220,29 @@ 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
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
|
||||
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
|
||||
|
||||
77
docs/azure-policy/azurepolicy.ebnf
Normal file
77
docs/azure-policy/azurepolicy.ebnf
Normal file
@@ -0,0 +1,77 @@
|
||||
(* 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 ']'
|
||||
@@ -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 `bincode` using helper
|
||||
3. **Preamble payloads**: each section is encoded with `postcard` using helper
|
||||
wrappers (`BinaryValueSlice`, `BinaryValueRef`) to stream complex `Value`
|
||||
graphs without cloning.
|
||||
4. **Program core**: the remaining `Program` struct is serialized once more via
|
||||
`bincode`; fields skipped by serde (entry points, literals, sources,
|
||||
`postcard`; fields skipped by serde (entry points, literals, sources,
|
||||
rule_tree, resolved builtins) are re-inserted from the preamble when the
|
||||
program is reconstructed.
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#[cfg(not(any(target_family = "wasm")))]
|
||||
#[cfg(not(any(target_family = "wasm", miri)))]
|
||||
pub mod mimalloc;
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
#[cfg(not(any(target_family = "wasm")))]
|
||||
#[cfg(not(any(target_family = "wasm", miri)))]
|
||||
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")))]
|
||||
#[cfg(not(any(target_family = "wasm", miri)))]
|
||||
pub mod limits;
|
||||
|
||||
/// Declare a global allocator if the platform supports it.
|
||||
#[macro_export]
|
||||
macro_rules! assign_global {
|
||||
() => {
|
||||
#[cfg(not(any(target_family = "wasm")))]
|
||||
#[cfg(not(any(target_family = "wasm", miri)))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::mimalloc::Mimalloc = mimalloc::mimalloc::Mimalloc;
|
||||
};
|
||||
|
||||
151
src/builtins/azure_policy/helpers.rs
Normal file
151
src/builtins/azure_policy/helpers.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
// 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 ¤t {
|
||||
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
|
||||
}
|
||||
65
src/builtins/azure_policy/mod.rs
Normal file
65
src/builtins/azure_policy/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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);
|
||||
}
|
||||
116
src/builtins/azure_policy/operators.rs
Normal file
116
src/builtins/azure_policy/operators.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
// 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 = ¶ms_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())
|
||||
}
|
||||
}
|
||||
372
src/builtins/azure_policy/template_functions.rs
Normal file
372
src/builtins/azure_policy/template_functions.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
// 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)))
|
||||
}
|
||||
317
src/builtins/azure_policy/template_functions_collection.rs
Normal file
317
src/builtins/azure_policy/template_functions_collection.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
// 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
|
||||
}
|
||||
687
src/builtins/azure_policy/template_functions_datetime.rs
Normal file
687
src/builtins/azure_policy/template_functions_datetime.rs
Normal file
@@ -0,0 +1,687 @@
|
||||
// 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))
|
||||
}
|
||||
329
src/builtins/azure_policy/template_functions_encoding.rs
Normal file
329
src/builtins/azure_policy/template_functions_encoding.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
// 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)))
|
||||
}
|
||||
197
src/builtins/azure_policy/template_functions_misc.rs
Normal file
197
src/builtins/azure_policy/template_functions_misc.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
// 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
|
||||
}
|
||||
155
src/builtins/azure_policy/template_functions_numeric.rs
Normal file
155
src/builtins/azure_policy/template_functions_numeric.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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
|
||||
}
|
||||
474
src/builtins/azure_policy/template_functions_string.rs
Normal file
474
src/builtins/azure_policy/template_functions_string.rs
Normal file
@@ -0,0 +1,474 @@
|
||||
// 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(())
|
||||
}
|
||||
@@ -52,11 +52,33 @@ fn make_delimiters_unix_style(s: &str, delimiters: &[char]) -> Result<String> {
|
||||
}
|
||||
|
||||
fn make_glob(pattern: &str, span: &Span) -> Result<GlobMatcher> {
|
||||
Ok(GlobBuilder::new(pattern)
|
||||
.literal_separator(true)
|
||||
.build()
|
||||
.or_else(|_| bail!(span.error("invalid glob")))?
|
||||
.compile_matcher())
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
|
||||
fn glob_match(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
mod aggregates;
|
||||
mod arrays;
|
||||
#[cfg(feature = "azure_policy")]
|
||||
pub mod azure_policy;
|
||||
mod bitwise;
|
||||
pub mod comparison;
|
||||
mod conversions;
|
||||
@@ -104,6 +106,8 @@ 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);
|
||||
|
||||
@@ -19,7 +19,9 @@ use crate::*;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use vstd::prelude::*;
|
||||
|
||||
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
|
||||
m.insert("abs", (abs, 1));
|
||||
@@ -188,3 +190,13 @@ fn intn(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Res
|
||||
_ => Value::Undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// Prove properties with Verus
|
||||
|
||||
verus! {
|
||||
proof fn lemma_test_one_plus_one_equals_two()
|
||||
ensures
|
||||
1 + 1 == 2,
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,39 @@ 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",
|
||||
@@ -39,8 +72,8 @@ fn find_all_string_submatch_n(
|
||||
let value = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
let n = ensure_numeric(name, ¶ms[2], &args[2])?;
|
||||
|
||||
let pattern =
|
||||
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
let re = get_or_compile_regex(&pattern)
|
||||
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
|
||||
if !n.is_integer() {
|
||||
bail!(params[2].span().error("n must be an integer"));
|
||||
@@ -53,8 +86,7 @@ fn find_all_string_submatch_n(
|
||||
};
|
||||
|
||||
Ok(Value::from_array(
|
||||
pattern
|
||||
.captures_iter(&value)
|
||||
re.captures_iter(&value)
|
||||
.map(|capture| {
|
||||
let groups = capture
|
||||
.iter()
|
||||
@@ -86,8 +118,8 @@ fn find_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
|
||||
let value = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
let n = ensure_numeric(name, ¶ms[2], &args[2])?;
|
||||
|
||||
let pattern =
|
||||
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
let re = get_or_compile_regex(&pattern)
|
||||
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
|
||||
if !n.is_integer() {
|
||||
bail!(params[2].span().error("n must be an integer"));
|
||||
@@ -100,8 +132,7 @@ fn find_n(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
|
||||
};
|
||||
|
||||
Ok(Value::from_array(
|
||||
pattern
|
||||
.find_iter(&value)
|
||||
re.find_iter(&value)
|
||||
.map(|m| {
|
||||
let value = Value::String(m.as_str().into());
|
||||
// Guard match accumulation while pushing each substring.
|
||||
@@ -116,8 +147,11 @@ 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, ¶ms[0], &args[0])
|
||||
.map_or(Value::Bool(false), |p| Value::Bool(Regex::new(&p).is_ok())))
|
||||
Ok(
|
||||
ensure_string(name, ¶ms[0], &args[0]).map_or(Value::Bool(false), |p| {
|
||||
Value::Bool(get_or_compile_regex(&p).is_ok())
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn regex_match(
|
||||
@@ -131,9 +165,9 @@ pub fn regex_match(
|
||||
let pattern = ensure_string(name, ¶ms[0], &args[0])?;
|
||||
let value = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
|
||||
let pattern =
|
||||
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
Ok(Value::Bool(pattern.is_match(&value)))
|
||||
let re = get_or_compile_regex(&pattern)
|
||||
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
Ok(Value::Bool(re.is_match(&value)))
|
||||
}
|
||||
|
||||
fn regex_replace(
|
||||
@@ -149,15 +183,13 @@ fn regex_replace(
|
||||
let pattern = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
let value = ensure_string(name, ¶ms[2], &args[2])?;
|
||||
|
||||
let pattern = match Regex::new(&pattern) {
|
||||
let re = match get_or_compile_regex(&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(
|
||||
pattern.replace_all(&s, value.as_ref()).into(),
|
||||
))
|
||||
Ok(Value::String(re.replace_all(&s, value.as_ref()).into()))
|
||||
}
|
||||
|
||||
fn regex_split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
||||
@@ -166,11 +198,10 @@ fn regex_split(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool)
|
||||
let pattern = ensure_string(name, ¶ms[0], &args[0])?;
|
||||
let value = ensure_string(name, ¶ms[1], &args[1])?;
|
||||
|
||||
let pattern =
|
||||
Regex::new(&pattern).or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
let re = get_or_compile_regex(&pattern)
|
||||
.or_else(|_| bail!(params[0].span().error("invalid regex")))?;
|
||||
Ok(Value::from_array(
|
||||
pattern
|
||||
.split(&value)
|
||||
re.split(&value)
|
||||
.map(|s| {
|
||||
let value = Value::String(s.into());
|
||||
// Guard output accumulation as each split segment is emitted.
|
||||
@@ -211,13 +242,13 @@ fn regex_template_match(
|
||||
}
|
||||
|
||||
// Fetch pattern, excluding delimiters.
|
||||
let pattern = Regex::new(&template[start + delimiter_start.len()..end])
|
||||
let re = get_or_compile_regex(&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 pattern.find(value) {
|
||||
let m = match re.find(value) {
|
||||
Some(m) if m.start() == 0 => m,
|
||||
_ => return Ok(Value::Bool(false)),
|
||||
};
|
||||
|
||||
171
src/cache.rs
Normal file
171
src/cache.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -9,6 +9,7 @@ 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::*;
|
||||
@@ -26,6 +27,7 @@ pub struct Engine {
|
||||
prepared: bool,
|
||||
rego_v1: bool,
|
||||
execution_timer_config: Option<ExecutionTimerConfig>,
|
||||
policy_length_config: PolicyLengthConfig,
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
@@ -83,6 +85,7 @@ impl Engine {
|
||||
prepared: false,
|
||||
rego_v1: true,
|
||||
execution_timer_config: None,
|
||||
policy_length_config: PolicyLengthConfig::default(),
|
||||
};
|
||||
engine.apply_effective_execution_timer_config();
|
||||
engine
|
||||
@@ -144,6 +147,31 @@ 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
|
||||
@@ -171,6 +199,20 @@ 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.
|
||||
@@ -198,7 +240,12 @@ impl Engine {
|
||||
/// ```
|
||||
///
|
||||
pub fn add_policy(&mut self, path: String, rego: String) -> Result<String> {
|
||||
let source = Source::from_contents(path, rego)?;
|
||||
let source = Source::from_contents_with_limits(
|
||||
path,
|
||||
rego,
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
let mut parser = self.make_parser(&source)?;
|
||||
let module = Ref::new(parser.parse()?);
|
||||
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
||||
@@ -232,7 +279,11 @@ 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(path)?;
|
||||
let source = Source::from_file_with_limits(
|
||||
path,
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
let mut parser = self.make_parser(&source)?;
|
||||
let module = Ref::new(parser.parse()?);
|
||||
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
||||
@@ -918,15 +969,24 @@ impl Engine {
|
||||
|
||||
fn make_query(&mut self, query: String) -> Result<(NodeRef<Module>, NodeRef<Query>, Schedule)> {
|
||||
let mut query_module = {
|
||||
let source = Source::from_contents(
|
||||
let source = Source::from_contents_with_limits(
|
||||
"<query_module.rego>".to_owned(),
|
||||
"package __internal_query_module".to_owned(),
|
||||
self.policy_length_config.max_file_bytes,
|
||||
self.policy_length_config.max_lines,
|
||||
)?;
|
||||
Parser::new(&source)?.parse()?
|
||||
let mut parser = Parser::new(&source)?;
|
||||
parser.set_max_col(self.policy_length_config.max_col);
|
||||
parser.parse()?
|
||||
};
|
||||
|
||||
// Parse the query.
|
||||
let query_source = Source::from_contents("<query.rego>".to_string(), 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 mut parser = self.make_parser(&query_source)?;
|
||||
let query_node = parser.parse_user_query()?;
|
||||
query_module.num_expressions = parser.num_expressions();
|
||||
@@ -1506,6 +1566,7 @@ 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()?;
|
||||
}
|
||||
@@ -1524,6 +1585,7 @@ 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
|
||||
|
||||
@@ -289,7 +289,7 @@ impl Interpreter {
|
||||
}
|
||||
|
||||
fn execution_timer_tick(&mut self, work_units: u32) -> Result<()> {
|
||||
if self.execution_timer.limit().is_none() {
|
||||
if !self.execution_timer.accumulate(work_units) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ impl Interpreter {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.execution_timer.tick(work_units, now)?;
|
||||
self.execution_timer.check_now(now)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -402,13 +402,13 @@ impl Interpreter {
|
||||
self.reset_execution_timer_state();
|
||||
}
|
||||
|
||||
#[cfg(feature = "allocator-memory-limits")]
|
||||
#[cfg(all(feature = "allocator-memory-limits", not(miri)))]
|
||||
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(not(feature = "allocator-memory-limits"))]
|
||||
#[cfg(any(miri, 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(())
|
||||
|
||||
114
src/languages/azure_policy/aliases/denormalizer/casing.rs
Normal file
114
src/languages/azure_policy/aliases/denormalizer/casing.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user