Files
regorus/src/languages/azure_policy/parser/policy_definition.rs
T
Anand Krishnamoorthi 86b4a279fa fix(ffi): eliminate aliasing UB + add Azure Policy JSON compilation FFI (#727)
* fix(ffi): eliminate aliasing UB via to_shared_ref migration

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

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

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

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

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

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

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

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-22 12:50:16 -05:00

459 lines
16 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Parsing for the full Azure Policy definition envelope.
//!
//! Handles the outer `{ "properties": { ... } }` wrapper, extracting typed
//! fields (`displayName`, `description`, `mode`, `parameters`, `policyRule`)
//! and collecting everything else into `extra`.
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use crate::lexer::Span;
use crate::languages::azure_policy::ast::{
JsonValue, ObjectEntry, ParameterDefinition, PolicyDefinition, PolicyRule,
};
use super::core::Parser;
use super::error::ParseError;
impl<'source> Parser<'source> {
/// Parse a full Azure Policy definition JSON.
///
/// Accepts two forms:
/// 1. **Wrapped**: `{ "properties": { ... }, ... }` — the standard ARM resource format.
/// 2. **Unwrapped**: `{ "displayName": ..., "policyRule": ..., ... }` — just the
/// `properties` contents directly.
///
/// In the wrapped form, fields outside `properties` (like `id`, `name`, `type`)
/// are collected into `extra`.
///
/// Detection: if the top-level object has a `"properties"` key whose value
/// is a JSON object, the wrapped path is taken. (In practice, the ARM
/// resource envelope always uses `"properties"` to wrap the definition body.)
pub fn parse_policy_definition(&mut self) -> Result<PolicyDefinition, ParseError> {
let open = self.expect_symbol("{")?;
// Determine whether this looks like the wrapped ARM resource form by
// scanning top-level keys as they are parsed.
//
// Because JSON keys can appear in any order, we handle the general case:
// iterate all top-level keys; if we encounter "properties" whose value is
// an object, take the wrapped path. Keys outside "properties" go into
// `extra`.
//
// For the unwrapped form, all top-level keys are treated as
// properties-level fields.
let mut display_name: Option<String> = None;
let mut description: Option<String> = None;
let mut mode: Option<String> = None;
let mut metadata: Option<JsonValue> = None;
let mut parameters: Vec<ParameterDefinition> = Vec::new();
let mut policy_rule: Option<PolicyRule> = None;
let mut extra = Vec::new();
// Track whether we found a "properties" object (wrapped form).
let mut found_properties = false;
// Track whether we have seen any "properties" key (for duplicate detection).
let mut seen_properties = false;
// Track seen recognized keys for duplicate detection (unwrapped path).
let mut seen_keys: Vec<String> = Vec::new();
if self.token_text() != "}" {
loop {
let (key_span, key) = self.expect_string()?;
self.expect_symbol(":")?;
match key.to_lowercase().as_str() {
"properties" if seen_properties => {
return Err(ParseError::DuplicateKey {
span: key_span,
key,
});
}
"properties" => {
seen_properties = true;
if self.token_text() == "{" {
// Wrapped form: parse inner properties object structurally.
found_properties = true;
self.parse_properties_fields(
&mut display_name,
&mut description,
&mut mode,
&mut metadata,
&mut parameters,
&mut policy_rule,
&mut extra,
&mut seen_keys,
)?;
} else {
// Unwrapped form: "properties" is just a regular key.
Self::handle_properties_key(
self,
key_span,
&key,
&mut display_name,
&mut description,
&mut mode,
&mut metadata,
&mut parameters,
&mut policy_rule,
&mut extra,
&mut seen_keys,
)?;
}
}
_ if !found_properties => {
// Unwrapped form: dispatch on properties-level keys.
Self::handle_properties_key(
self,
key_span,
&key,
&mut display_name,
&mut description,
&mut mode,
&mut metadata,
&mut parameters,
&mut policy_rule,
&mut extra,
&mut seen_keys,
)?;
}
_ => {
// Wrapped form: keys outside "properties" go to extra.
let value = self.parse_json_value()?;
extra.push(ObjectEntry {
key_span,
key,
value,
});
}
}
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
let close = self.expect_symbol("}")?;
let span = Span {
source: open.source.clone(),
line: open.line,
col: open.col,
start: open.start,
end: close.end,
};
let policy_rule = policy_rule.ok_or_else(|| ParseError::MissingKey {
span: span.clone(),
key: "policyRule",
})?;
Ok(PolicyDefinition {
span,
display_name,
description,
mode,
metadata,
parameters,
policy_rule,
extra,
})
}
/// Parse the interior of a `"properties": { ... }` object, dispatching
/// recognized keys to their typed parsers.
#[allow(clippy::too_many_arguments)]
fn parse_properties_fields(
&mut self,
display_name: &mut Option<String>,
description: &mut Option<String>,
mode: &mut Option<String>,
metadata: &mut Option<JsonValue>,
parameters: &mut Vec<ParameterDefinition>,
policy_rule: &mut Option<PolicyRule>,
extra: &mut Vec<ObjectEntry>,
seen_keys: &mut Vec<String>,
) -> Result<(), ParseError> {
self.expect_symbol("{")?;
if self.token_text() != "}" {
loop {
let (key_span, key) = self.expect_string()?;
self.expect_symbol(":")?;
Self::handle_properties_key(
self,
key_span,
&key,
display_name,
description,
mode,
metadata,
parameters,
policy_rule,
extra,
seen_keys,
)?;
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
self.expect_symbol("}")?;
Ok(())
}
/// Handle a single properties-level key, dispatching to the appropriate
/// typed parser or collecting into `extra`.
#[allow(clippy::too_many_arguments)]
fn handle_properties_key(
&mut self,
key_span: Span,
key: &str,
display_name: &mut Option<String>,
description: &mut Option<String>,
mode: &mut Option<String>,
metadata: &mut Option<JsonValue>,
parameters: &mut Vec<ParameterDefinition>,
policy_rule: &mut Option<PolicyRule>,
extra: &mut Vec<ObjectEntry>,
seen_keys: &mut Vec<String>,
) -> Result<(), ParseError> {
let lk = key.to_lowercase();
// Reject duplicate recognized property keys regardless of value type.
let recognized = matches!(
lk.as_str(),
"displayname" | "description" | "mode" | "metadata" | "parameters" | "policyrule"
);
if recognized {
if seen_keys.contains(&lk) {
return Err(ParseError::DuplicateKey {
span: key_span,
key: key.into(),
});
}
seen_keys.push(lk.clone());
}
match lk.as_str() {
"displayname" => {
let value = self.parse_json_value()?;
assign_string_or_extra(value, display_name, key_span, key, extra);
}
"description" => {
let value = self.parse_json_value()?;
assign_string_or_extra(value, description, key_span, key, extra);
}
"mode" => {
let value = self.parse_json_value()?;
assign_string_or_extra(value, mode, key_span, key, extra);
}
"metadata" => {
*metadata = Some(self.parse_json_value()?);
}
"parameters" if self.token_text() == "{" => {
// Parameters must be a JSON object; if not, push to extra.
*parameters = self.parse_parameter_definitions()?;
}
"parameters" => {
let value = self.parse_json_value()?;
extra.push(ObjectEntry {
key_span,
key: key.into(),
value,
});
}
"policyrule" => {
// Parse the policyRule directly from the token stream!
*policy_rule = Some(self.parse_policy_rule()?);
}
_ => {
let value = self.parse_json_value()?;
extra.push(ObjectEntry {
key_span,
key: key.into(),
value,
});
}
}
Ok(())
}
/// Parse a `"parameters": { ... }` object into a list of
/// [`ParameterDefinition`], consuming tokens directly from the stream.
fn parse_parameter_definitions(&mut self) -> Result<Vec<ParameterDefinition>, ParseError> {
self.expect_symbol("{")?;
let mut defs = Vec::new();
let mut seen_names = BTreeSet::new();
if self.token_text() != "}" {
loop {
let param = self.parse_single_parameter()?;
if !seen_names.insert(param.name.to_lowercase()) {
return Err(ParseError::DuplicateKey {
span: param.name_span.clone(),
key: param.name.clone(),
});
}
defs.push(param);
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
self.expect_symbol("}")?;
Ok(defs)
}
/// Parse a single `"paramName": { ... }` entry.
fn parse_single_parameter(&mut self) -> Result<ParameterDefinition, ParseError> {
let (name_span, name) = self.expect_string()?;
self.expect_symbol(":")?;
// Parameter definitions must be JSON objects.
if self.token_text() != "{" {
return Err(ParseError::Custom {
span: name_span,
message: alloc::format!("expected object for parameter '{}'", name),
});
}
let open = self.expect_symbol("{")?;
let mut param_type: Option<String> = None;
let mut default_value: Option<JsonValue> = None;
let mut allowed_values: Option<Vec<JsonValue>> = None;
let mut metadata: Option<JsonValue> = None;
let mut extra = Vec::new();
// Track seen recognized keys for duplicate detection.
let mut seen_type = false;
let mut seen_default_value = false;
let mut seen_allowed_values = false;
let mut seen_metadata = false;
if self.token_text() != "}" {
loop {
let (ks, k) = self.expect_string()?;
self.expect_symbol(":")?;
match k.to_lowercase().as_str() {
"type" => {
if seen_type {
return Err(ParseError::DuplicateKey { span: ks, key: k });
}
seen_type = true;
let value = self.parse_json_value()?;
assign_string_or_extra(value, &mut param_type, ks, &k, &mut extra);
}
"defaultvalue" => {
if seen_default_value {
return Err(ParseError::DuplicateKey { span: ks, key: k });
}
seen_default_value = true;
default_value = Some(self.parse_json_value()?);
}
"allowedvalues" => {
if seen_allowed_values {
return Err(ParseError::DuplicateKey { span: ks, key: k });
}
seen_allowed_values = true;
let value = self.parse_json_value()?;
if let JsonValue::Array(_, items) = value {
allowed_values = Some(items);
} else {
extra.push(ObjectEntry {
key_span: ks,
key: k,
value,
});
}
}
"metadata" => {
if seen_metadata {
return Err(ParseError::DuplicateKey { span: ks, key: k });
}
seen_metadata = true;
metadata = Some(self.parse_json_value()?);
}
_ => {
let value = self.parse_json_value()?;
extra.push(ObjectEntry {
key_span: ks,
key: k,
value,
});
}
}
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
let close = self.expect_symbol("}")?;
let span = Span {
source: open.source.clone(),
line: open.line,
col: open.col,
start: open.start,
end: close.end,
};
Ok(ParameterDefinition {
span,
name,
name_span,
param_type,
default_value,
allowed_values,
metadata,
extra,
})
}
}
// ============================================================================
// Helpers
// ============================================================================
/// If `value` is a plain string, assign it to `target`; otherwise push
/// the entry to `extra` so callers don't have to repeat this pattern.
fn assign_string_or_extra(
value: JsonValue,
target: &mut Option<String>,
key_span: Span,
key: &str,
extra: &mut Vec<ObjectEntry>,
) {
if let JsonValue::Str(_, ref s) = value {
*target = Some(s.clone());
} else {
extra.push(ObjectEntry {
key_span,
key: key.into(),
value,
});
}
}