mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(rvm): implement Azure Policy condition evaluation (#661)
Add VM support for Azure Policy's condition operators and allOf/anyOf short-circuit logic, gated behind cfg(feature = "azure_policy"). Policy conditions (equals, contains, like, match, exists, and their negations — 21 total) are encoded as a single PolicyCondition instruction with a PolicyOp sub-opcode rather than bloating the Instruction enum with 21 variants. The dispatch handles Azure Policy's quirky comparison semantics: case-insensitive string comparison, string↔number coercion, null vs undefined distinction, and element-wise collection membership. allOf/anyOf blocks use four instructions — LogicalBlockStart, AllOfNext/AnyOfNext, and LogicalBlockEnd — that wire up a result register and short-circuit on the first failing (allOf) or passing (anyOf) child. Helper functions for case-folded comparison, wildcard/glob matching, and type coercion live in builtins::azure_policy::helpers. Two YAML test suites (~2200 lines) exercise the full operator matrix and the allOf/anyOf control flow.
This commit is contained in:
committed by
GitHub
parent
83ce8c3580
commit
4d35744c4f
@@ -47,6 +47,156 @@ pub fn as_str(value: &Value) -> Option<&str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Coerce a value to its string representation for policy comparison operators.
|
||||||
|
///
|
||||||
|
/// Azure Policy coerces numbers and booleans to strings when used with string
|
||||||
|
/// operators (`like`, `match`, `contains`, `matchInsensitively`). This is
|
||||||
|
/// needed when, for example, a count result (always a number) is compared
|
||||||
|
/// using a string operator: `count(...) like 2`.
|
||||||
|
pub fn coerce_to_string(value: &Value) -> Option<String> {
|
||||||
|
match *value {
|
||||||
|
Value::String(ref s) => Some(s.to_string()),
|
||||||
|
Value::Number(ref n) => Some(n.format_decimal()),
|
||||||
|
Value::Bool(b) => Some(if b { "true" } else { "false" }.to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn coerce_to_string_ci(value: &Value) -> Option<String> {
|
||||||
|
coerce_to_string(value).map(|s| strings::case_fold::fold(&s).into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collection helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Check if an array or set contains a null sentinel.
|
||||||
|
pub fn collection_has_null(v: &Value) -> bool {
|
||||||
|
match *v {
|
||||||
|
Value::Array(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
|
||||||
|
Value::Set(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any non-null element in a collection case-insensitively equals `target`.
|
||||||
|
/// Scalar RHS is treated as a single-element collection.
|
||||||
|
pub fn collection_any_ci_eq_excluding_null(collection: &Value, target: &Value) -> bool {
|
||||||
|
match *collection {
|
||||||
|
Value::Array(ref items) => items
|
||||||
|
.iter()
|
||||||
|
.filter(|i| !matches!(i, Value::Null))
|
||||||
|
.any(|i| case_insensitive_equals(i, target)),
|
||||||
|
Value::Set(ref items) => items
|
||||||
|
.iter()
|
||||||
|
.filter(|i| !matches!(i, Value::Null))
|
||||||
|
.any(|i| case_insensitive_equals(i, target)),
|
||||||
|
_ => case_insensitive_equals(collection, target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_boolish(value: &Value) -> Option<bool> {
|
||||||
|
match *value {
|
||||||
|
Value::Bool(b) => Some(b),
|
||||||
|
Value::String(ref s) => {
|
||||||
|
if s.eq_ignore_ascii_case("true") {
|
||||||
|
Some(true)
|
||||||
|
} else if s.eq_ignore_ascii_case("false") {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Comparison and coercion ───────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn compare_values(left: &Value, right: &Value) -> Option<i8> {
|
||||||
|
if is_undefined(left) || is_undefined(right) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::pattern_type_mismatch)]
|
||||||
|
match (left, right) {
|
||||||
|
(Value::String(a), Value::String(b)) => Some(match strings::case_fold::cmp(a, b) {
|
||||||
|
core::cmp::Ordering::Less => -1,
|
||||||
|
core::cmp::Ordering::Equal => 0,
|
||||||
|
core::cmp::Ordering::Greater => 1,
|
||||||
|
}),
|
||||||
|
(Value::Number(a), Value::Number(b)) => Some(if a < b {
|
||||||
|
-1
|
||||||
|
} else if a > b {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}),
|
||||||
|
(Value::Bool(a), Value::Bool(b)) => Some(if a == b {
|
||||||
|
0
|
||||||
|
} else if !a && *b {
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}),
|
||||||
|
// String ↔ Number coercion
|
||||||
|
(Value::String(s), Value::Number(n)) => try_coerce_to_number(s).map(|sn| {
|
||||||
|
if &sn < n {
|
||||||
|
-1
|
||||||
|
} else if &sn > n {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(Value::Number(n), Value::String(s)) => try_coerce_to_number(s).map(|sn| {
|
||||||
|
if n < &sn {
|
||||||
|
-1
|
||||||
|
} else if n > &sn {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn case_insensitive_equals(left: &Value, right: &Value) -> bool {
|
||||||
|
if is_undefined(left) || is_undefined(right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Azure Policy treats an explicit null field value as "" (empty string)
|
||||||
|
// for comparison purposes. Missing fields are Undefined and caught above.
|
||||||
|
#[allow(clippy::pattern_type_mismatch)]
|
||||||
|
match (left, right) {
|
||||||
|
(Value::Null, Value::Null) => true,
|
||||||
|
(Value::Null, Value::String(b)) => strings::case_fold::eq("", b),
|
||||||
|
(Value::String(a), Value::Null) => strings::case_fold::eq(a, ""),
|
||||||
|
(Value::String(a), Value::String(b)) => strings::case_fold::eq(a, b),
|
||||||
|
// String ↔ Number coercion
|
||||||
|
(Value::String(s), Value::Number(_)) | (Value::Number(_), Value::String(s)) => {
|
||||||
|
try_coerce_to_number(s).is_some_and(|n| {
|
||||||
|
let num_val = Value::Number(n);
|
||||||
|
let other = if matches!(left, Value::String(_)) {
|
||||||
|
right
|
||||||
|
} else {
|
||||||
|
left
|
||||||
|
};
|
||||||
|
&num_val == other
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// String ↔ Bool coercion ("true"/"false" ↔ true/false)
|
||||||
|
(Value::String(_), Value::Bool(b)) | (Value::Bool(b), Value::String(_)) => {
|
||||||
|
as_boolish(if matches!(left, Value::String(_)) {
|
||||||
|
left
|
||||||
|
} else {
|
||||||
|
right
|
||||||
|
}) == Some(*b)
|
||||||
|
}
|
||||||
|
_ => left == right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Try to parse a string as a number for Azure Policy type coercion.
|
/// 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> {
|
pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
|
||||||
use core::str::FromStr as _;
|
use core::str::FromStr as _;
|
||||||
@@ -61,6 +211,103 @@ pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Pattern matching ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn match_pattern(input_val: &Value, pattern_val: &Value, insensitive: bool) -> bool {
|
||||||
|
let Some(mut input) = coerce_to_string(input_val) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(mut pattern) = coerce_to_string(pattern_val) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if insensitive {
|
||||||
|
input = strings::case_fold::fold(&input).into_owned();
|
||||||
|
pattern = strings::case_fold::fold(&pattern).into_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
match_question_hash_pattern(&input, &pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match_like_pattern_ci(input: &str, pattern: &str) -> bool {
|
||||||
|
wildcard_match(input, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_char(s: &str, index: usize) -> Option<(char, usize)> {
|
||||||
|
s.get(index..)?
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map(|ch| (ch, index.saturating_add(ch.len_utf8())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wildcard_match(input: &str, pattern: &str) -> bool {
|
||||||
|
let (mut ii, mut pi) = (0_usize, 0_usize);
|
||||||
|
let mut star_pat: Option<usize> = None;
|
||||||
|
let mut star_inp = 0_usize;
|
||||||
|
|
||||||
|
while ii < input.len() {
|
||||||
|
let pat = next_char(pattern, pi);
|
||||||
|
let inp = next_char(input, ii);
|
||||||
|
|
||||||
|
if let (Some((pc, next_pi)), Some((ic, next_ii))) = (pat, inp) {
|
||||||
|
if pc == '?' || pc == ic {
|
||||||
|
pi = next_pi;
|
||||||
|
ii = next_ii;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(pat, Some(('*', _))) {
|
||||||
|
star_pat = Some(pi);
|
||||||
|
star_inp = ii;
|
||||||
|
pi = pi.saturating_add('*'.len_utf8());
|
||||||
|
} else if let Some(saved_pi) = star_pat {
|
||||||
|
pi = saved_pi.saturating_add('*'.len_utf8());
|
||||||
|
if let Some((_, next_ii)) = next_char(input, star_inp) {
|
||||||
|
star_inp = next_ii;
|
||||||
|
ii = star_inp;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while matches!(next_char(pattern, pi), Some(('*', _))) {
|
||||||
|
pi = pi.saturating_add('*'.len_utf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
pi == pattern.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match_question_hash_pattern(input: &str, pattern: &str) -> bool {
|
||||||
|
let mut input_chars = input.chars();
|
||||||
|
let mut pattern_chars = pattern.chars();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match (input_chars.next(), pattern_chars.next()) {
|
||||||
|
(None, None) => return true,
|
||||||
|
(Some(_), None) | (None, Some(_)) => return false,
|
||||||
|
(Some(input_char), Some(pattern_char)) => {
|
||||||
|
if pattern_char == '.' {
|
||||||
|
// '.' matches any single character (letter, digit, or special).
|
||||||
|
} else if pattern_char == '#' {
|
||||||
|
if !input_char.is_ascii_digit() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if pattern_char == '?' {
|
||||||
|
if !input_char.is_ascii_alphabetic() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if input_char != pattern_char {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Path resolution ───────────────────────────────────────────────────
|
// ── Path resolution ───────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn resolve_path(root: &Value, path: &str) -> Value {
|
pub fn resolve_path(root: &Value, path: &str) -> Value {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use alloc::format;
|
|||||||
use alloc::string::String;
|
use alloc::string::String;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use super::types::GuardMode;
|
use super::types::{GuardMode, LogicalBlockMode, PolicyOp};
|
||||||
use super::{Instruction, InstructionData, LiteralOrRegister};
|
use super::{Instruction, InstructionData, LiteralOrRegister};
|
||||||
|
|
||||||
impl Instruction {
|
impl Instruction {
|
||||||
@@ -291,6 +291,51 @@ impl core::fmt::Display for Instruction {
|
|||||||
|k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg),
|
|k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg),
|
||||||
),
|
),
|
||||||
Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"),
|
Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"),
|
||||||
|
|
||||||
|
// Azure Policy consolidated instruction
|
||||||
|
Instruction::PolicyCondition {
|
||||||
|
dest,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
op,
|
||||||
|
} => match op {
|
||||||
|
PolicyOp::Not => format!("{} R({}) R({})", op.display_name(), dest, left),
|
||||||
|
_ => format!("{} R({}) R({}) R({})", op.display_name(), dest, left, right),
|
||||||
|
},
|
||||||
|
|
||||||
|
// AllOf / AnyOf structured instructions
|
||||||
|
Instruction::LogicalBlockStart {
|
||||||
|
mode,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
} => {
|
||||||
|
let name = match mode {
|
||||||
|
LogicalBlockMode::AllOf => "ALL_OF_START",
|
||||||
|
LogicalBlockMode::AnyOf => "ANY_OF_START",
|
||||||
|
};
|
||||||
|
format!("{} R({}) {}", name, result, end_pc)
|
||||||
|
}
|
||||||
|
Instruction::AllOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
} => {
|
||||||
|
format!("ALL_OF_NEXT R({}) R({}) {}", check, result, end_pc)
|
||||||
|
}
|
||||||
|
Instruction::AnyOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
} => {
|
||||||
|
format!("ANY_OF_NEXT R({}) R({}) {}", check, result, end_pc)
|
||||||
|
}
|
||||||
|
Instruction::LogicalBlockEnd { mode, result } => {
|
||||||
|
let name = match mode {
|
||||||
|
LogicalBlockMode::AllOf => "ALL_OF_END",
|
||||||
|
LogicalBlockMode::AnyOf => "ANY_OF_END",
|
||||||
|
};
|
||||||
|
format!("{} R({})", name, result)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
write!(f, "{}", text)
|
write!(f, "{}", text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ pub use params::{
|
|||||||
FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams,
|
FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams,
|
||||||
VirtualDataDocumentLookupParams,
|
VirtualDataDocumentLookupParams,
|
||||||
};
|
};
|
||||||
pub use types::{ComprehensionMode, GuardMode, LiteralOrRegister, LoopMode};
|
pub use types::{
|
||||||
|
ComprehensionMode, GuardMode, LiteralOrRegister, LogicalBlockMode, LoopMode, PolicyOp,
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -355,6 +357,65 @@ pub enum Instruction {
|
|||||||
|
|
||||||
/// End a comprehension block
|
/// End a comprehension block
|
||||||
ComprehensionEnd {},
|
ComprehensionEnd {},
|
||||||
|
|
||||||
|
// ── Azure Policy condition operators (consolidated) ────────────────
|
||||||
|
/// Consolidated Azure Policy condition instruction.
|
||||||
|
///
|
||||||
|
/// Replaces 21 separate Policy* variants. The `op` discriminant selects
|
||||||
|
/// the specific Azure Policy condition semantics.
|
||||||
|
///
|
||||||
|
/// For most ops: `dest = op(left, right)`.
|
||||||
|
/// For `PolicyOp::Not`: `dest = !is_true(left)`, `right` is unused (0).
|
||||||
|
/// For `PolicyOp::ValueConditionGuard`: `left` = value register,
|
||||||
|
/// `right` = condition register.
|
||||||
|
PolicyCondition {
|
||||||
|
dest: u8,
|
||||||
|
left: u8,
|
||||||
|
right: u8,
|
||||||
|
op: PolicyOp,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── AllOf / AnyOf structured short-circuit instructions ───────────
|
||||||
|
/// Initialize allOf/anyOf: set result register to false.
|
||||||
|
LogicalBlockStart {
|
||||||
|
mode: LogicalBlockMode,
|
||||||
|
/// Register that accumulates the result.
|
||||||
|
result: u8,
|
||||||
|
/// PC of the corresponding End instruction.
|
||||||
|
end_pc: u16,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Check one allOf child: if not true, short-circuit (result stays false),
|
||||||
|
/// jump to end_pc.
|
||||||
|
AllOfNext {
|
||||||
|
/// Register holding the child condition result.
|
||||||
|
check: u8,
|
||||||
|
/// Register that accumulates the allOf result.
|
||||||
|
result: u8,
|
||||||
|
/// PC of the AllOfEnd instruction (jump target on short-circuit).
|
||||||
|
end_pc: u16,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Check one anyOf child: if true, short-circuit (set result to true),
|
||||||
|
/// jump to end_pc.
|
||||||
|
AnyOfNext {
|
||||||
|
/// Register holding the child condition result.
|
||||||
|
check: u8,
|
||||||
|
/// Register that accumulates the anyOf result.
|
||||||
|
result: u8,
|
||||||
|
/// PC of the AnyOfEnd instruction.
|
||||||
|
end_pc: u16,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Finalize allOf/anyOf block.
|
||||||
|
///
|
||||||
|
/// For AllOf: all children passed → set result to true.
|
||||||
|
/// For AnyOf: no child matched → result stays false (no-op).
|
||||||
|
LogicalBlockEnd {
|
||||||
|
mode: LogicalBlockMode,
|
||||||
|
/// Register that accumulates the result.
|
||||||
|
result: u8,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instruction {
|
impl Instruction {
|
||||||
|
|||||||
@@ -46,6 +46,110 @@ pub enum ComprehensionMode {
|
|||||||
Object,
|
Object,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Azure Policy condition operator sub-opcodes.
|
||||||
|
///
|
||||||
|
/// Each variant maps to one of the ~21 Azure Policy condition operators.
|
||||||
|
/// Stored inside `Instruction::PolicyCondition` to collapse 21 enum variants
|
||||||
|
/// into a single instruction with a sub-op discriminant.
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum PolicyOp {
|
||||||
|
Equals,
|
||||||
|
NotEquals,
|
||||||
|
Greater,
|
||||||
|
GreaterOrEquals,
|
||||||
|
Less,
|
||||||
|
LessOrEquals,
|
||||||
|
In,
|
||||||
|
NotIn,
|
||||||
|
Contains,
|
||||||
|
NotContains,
|
||||||
|
ContainsKey,
|
||||||
|
NotContainsKey,
|
||||||
|
Like,
|
||||||
|
NotLike,
|
||||||
|
Match,
|
||||||
|
NotMatch,
|
||||||
|
MatchInsensitively,
|
||||||
|
NotMatchInsensitively,
|
||||||
|
Exists,
|
||||||
|
/// Guard for `value:` conditions — forces false when LHS is undefined.
|
||||||
|
/// Uses `left` = value register, `right` = condition register.
|
||||||
|
ValueConditionGuard,
|
||||||
|
/// Logical negation: `!is_true(operand)`. Uses `left` = operand, `right` is unused (0).
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PolicyOp {
|
||||||
|
/// Display name used in assembly listings and Debug output.
|
||||||
|
pub const fn display_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Equals => "POLICY_EQUALS",
|
||||||
|
Self::NotEquals => "POLICY_NOT_EQUALS",
|
||||||
|
Self::Greater => "POLICY_GREATER",
|
||||||
|
Self::GreaterOrEquals => "POLICY_GREATER_OR_EQUALS",
|
||||||
|
Self::Less => "POLICY_LESS",
|
||||||
|
Self::LessOrEquals => "POLICY_LESS_OR_EQUALS",
|
||||||
|
Self::In => "POLICY_IN",
|
||||||
|
Self::NotIn => "POLICY_NOT_IN",
|
||||||
|
Self::Contains => "POLICY_CONTAINS",
|
||||||
|
Self::NotContains => "POLICY_NOT_CONTAINS",
|
||||||
|
Self::ContainsKey => "POLICY_CONTAINS_KEY",
|
||||||
|
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
|
||||||
|
Self::Like => "POLICY_LIKE",
|
||||||
|
Self::NotLike => "POLICY_NOT_LIKE",
|
||||||
|
Self::Match => "POLICY_MATCH",
|
||||||
|
Self::NotMatch => "POLICY_NOT_MATCH",
|
||||||
|
Self::MatchInsensitively => "POLICY_MATCH_INSENSITIVELY",
|
||||||
|
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_INSENSITIVELY",
|
||||||
|
Self::Exists => "POLICY_EXISTS",
|
||||||
|
Self::ValueConditionGuard => "VALUE_CONDITION_GUARD",
|
||||||
|
Self::Not => "POLICY_NOT",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compact name for tabular assembly listings.
|
||||||
|
pub const fn compact_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Equals => "POLICY_EQ",
|
||||||
|
Self::NotEquals => "POLICY_NE",
|
||||||
|
Self::Greater => "POLICY_GT",
|
||||||
|
Self::GreaterOrEquals => "POLICY_GE",
|
||||||
|
Self::Less => "POLICY_LT",
|
||||||
|
Self::LessOrEquals => "POLICY_LE",
|
||||||
|
Self::In => "POLICY_IN",
|
||||||
|
Self::NotIn => "POLICY_NOT_IN",
|
||||||
|
Self::Contains => "POLICY_CONTAINS",
|
||||||
|
Self::NotContains => "POLICY_NOT_CONTAINS",
|
||||||
|
Self::ContainsKey => "POLICY_CONTAINS_KEY",
|
||||||
|
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
|
||||||
|
Self::Like => "POLICY_LIKE",
|
||||||
|
Self::NotLike => "POLICY_NOT_LIKE",
|
||||||
|
Self::Match => "POLICY_MATCH",
|
||||||
|
Self::NotMatch => "POLICY_NOT_MATCH",
|
||||||
|
Self::MatchInsensitively => "POLICY_MATCH_CI",
|
||||||
|
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_CI",
|
||||||
|
Self::Exists => "POLICY_EXISTS",
|
||||||
|
Self::ValueConditionGuard => "VAL_COND_GUARD",
|
||||||
|
Self::Not => "POLICY_NOT",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` for negated condition operators (NotEquals, NotIn, etc.).
|
||||||
|
pub const fn is_negated(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::NotEquals
|
||||||
|
| Self::NotIn
|
||||||
|
| Self::NotContains
|
||||||
|
| Self::NotContainsKey
|
||||||
|
| Self::NotLike
|
||||||
|
| Self::NotMatch
|
||||||
|
| Self::NotMatchInsensitively
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Guard sub-modes for the consolidated `Guard` instruction.
|
/// Guard sub-modes for the consolidated `Guard` instruction.
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -57,3 +161,11 @@ pub enum GuardMode {
|
|||||||
/// Assert not undefined — fail (return undefined) if register is undefined.
|
/// Assert not undefined — fail (return undefined) if register is undefined.
|
||||||
NotUndefined,
|
NotUndefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mode discriminant for merged AllOf/AnyOf Start and End instructions.
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum LogicalBlockMode {
|
||||||
|
AllOf,
|
||||||
|
AnyOf,
|
||||||
|
}
|
||||||
|
|||||||
@@ -911,6 +911,15 @@ fn format_instruction_readable(
|
|||||||
let base = format!("{}}} CompEnd", indent);
|
let base = format!("{}}} CompEnd", indent);
|
||||||
align_comment(&base, "End comprehension block", config.comment_column)
|
align_comment(&base, "End comprehension block", config.comment_column)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Azure Policy & allOf/anyOf instructions — use Display impl
|
||||||
|
instruction @ Instruction::PolicyCondition { .. }
|
||||||
|
| instruction @ Instruction::LogicalBlockStart { .. }
|
||||||
|
| instruction @ Instruction::AllOfNext { .. }
|
||||||
|
| instruction @ Instruction::AnyOfNext { .. }
|
||||||
|
| instruction @ Instruction::LogicalBlockEnd { .. } => {
|
||||||
|
format!("{}{}", indent, instruction)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,6 +1054,19 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
|
|||||||
Instruction::ComprehensionBegin { .. } => "COMP_BEGIN",
|
Instruction::ComprehensionBegin { .. } => "COMP_BEGIN",
|
||||||
Instruction::ComprehensionYield { .. } => "COMP_YIELD",
|
Instruction::ComprehensionYield { .. } => "COMP_YIELD",
|
||||||
Instruction::ComprehensionEnd {} => "COMP_END",
|
Instruction::ComprehensionEnd {} => "COMP_END",
|
||||||
|
// Azure Policy instructions
|
||||||
|
Instruction::PolicyCondition { op, .. } => op.compact_name(),
|
||||||
|
// AllOf / AnyOf
|
||||||
|
Instruction::LogicalBlockStart { mode, .. } => match mode {
|
||||||
|
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_START",
|
||||||
|
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_START",
|
||||||
|
},
|
||||||
|
Instruction::AllOfNext { .. } => "ALL_OF_NEXT",
|
||||||
|
Instruction::AnyOfNext { .. } => "ANY_OF_NEXT",
|
||||||
|
Instruction::LogicalBlockEnd { mode, .. } => match mode {
|
||||||
|
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_END",
|
||||||
|
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_END",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
clippy::pattern_type_mismatch
|
clippy::pattern_type_mismatch
|
||||||
)] // tests unwrap conversions and slice math for brevity
|
)] // tests unwrap conversions and slice math for brevity
|
||||||
|
|
||||||
use crate::rvm::instructions::{GuardMode, Instruction, LoopMode};
|
use crate::rvm::instructions::{GuardMode, Instruction, LogicalBlockMode, LoopMode, PolicyOp};
|
||||||
use alloc::string::{String, ToString};
|
use alloc::string::{String, ToString};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
@@ -83,6 +83,41 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
|
|||||||
"ComprehensionYield" => parse_comprehension_add(params_text),
|
"ComprehensionYield" => parse_comprehension_add(params_text),
|
||||||
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
|
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
|
||||||
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
|
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
|
||||||
|
// Azure Policy condition operators
|
||||||
|
"PolicyEquals" => parse_policy_condition(params_text, PolicyOp::Equals),
|
||||||
|
"PolicyNotEquals" => parse_policy_condition(params_text, PolicyOp::NotEquals),
|
||||||
|
"PolicyGreater" => parse_policy_condition(params_text, PolicyOp::Greater),
|
||||||
|
"PolicyGreaterOrEquals" => {
|
||||||
|
parse_policy_condition(params_text, PolicyOp::GreaterOrEquals)
|
||||||
|
}
|
||||||
|
"PolicyLess" => parse_policy_condition(params_text, PolicyOp::Less),
|
||||||
|
"PolicyLessOrEquals" => parse_policy_condition(params_text, PolicyOp::LessOrEquals),
|
||||||
|
"PolicyIn" => parse_policy_condition(params_text, PolicyOp::In),
|
||||||
|
"PolicyNotIn" => parse_policy_condition(params_text, PolicyOp::NotIn),
|
||||||
|
"PolicyContains" => parse_policy_condition(params_text, PolicyOp::Contains),
|
||||||
|
"PolicyNotContains" => parse_policy_condition(params_text, PolicyOp::NotContains),
|
||||||
|
"PolicyContainsKey" => parse_policy_condition(params_text, PolicyOp::ContainsKey),
|
||||||
|
"PolicyNotContainsKey" => parse_policy_condition(params_text, PolicyOp::NotContainsKey),
|
||||||
|
"PolicyLike" => parse_policy_condition(params_text, PolicyOp::Like),
|
||||||
|
"PolicyNotLike" => parse_policy_condition(params_text, PolicyOp::NotLike),
|
||||||
|
"PolicyMatch" => parse_policy_condition(params_text, PolicyOp::Match),
|
||||||
|
"PolicyNotMatch" => parse_policy_condition(params_text, PolicyOp::NotMatch),
|
||||||
|
"PolicyMatchInsensitively" => {
|
||||||
|
parse_policy_condition(params_text, PolicyOp::MatchInsensitively)
|
||||||
|
}
|
||||||
|
"PolicyNotMatchInsensitively" => {
|
||||||
|
parse_policy_condition(params_text, PolicyOp::NotMatchInsensitively)
|
||||||
|
}
|
||||||
|
"PolicyExists" => parse_policy_condition(params_text, PolicyOp::Exists),
|
||||||
|
"ValueConditionGuard" => parse_value_condition_guard(params_text),
|
||||||
|
"PolicyNot" => parse_policy_not(params_text),
|
||||||
|
// AllOf / AnyOf
|
||||||
|
"AllOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AllOf),
|
||||||
|
"AllOfNext" => parse_allof_next(params_text),
|
||||||
|
"AllOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AllOf),
|
||||||
|
"AnyOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AnyOf),
|
||||||
|
"AnyOfNext" => parse_anyof_next(params_text),
|
||||||
|
"AnyOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AnyOf),
|
||||||
_ => bail!("Unknown instruction: {}", name),
|
_ => bail!("Unknown instruction: {}", name),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -707,3 +742,83 @@ fn parse_coalesce_undefined_to_null(params_text: &str) -> Result<Instruction> {
|
|||||||
register: register.try_into().unwrap(),
|
register: register.try_into().unwrap(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generic parser for PolicyCondition instructions with { dest, left, right } fields.
|
||||||
|
fn parse_policy_condition(params_text: &str, op: PolicyOp) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||||
|
let left: u8 = get_param_u16(¶ms, "left")?.try_into().unwrap();
|
||||||
|
let right: u8 = get_param_u16(¶ms, "right")?.try_into().unwrap();
|
||||||
|
Ok(Instruction::PolicyCondition {
|
||||||
|
dest,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
op,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_value_condition_guard(params_text: &str) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||||
|
let value: u8 = get_param_u16(¶ms, "value")?.try_into().unwrap();
|
||||||
|
let condition: u8 = get_param_u16(¶ms, "condition")?.try_into().unwrap();
|
||||||
|
Ok(Instruction::PolicyCondition {
|
||||||
|
dest,
|
||||||
|
left: value,
|
||||||
|
right: condition,
|
||||||
|
op: PolicyOp::ValueConditionGuard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_policy_not(params_text: &str) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||||
|
let operand: u8 = get_param_u16(¶ms, "operand")?.try_into().unwrap();
|
||||||
|
Ok(Instruction::PolicyCondition {
|
||||||
|
dest,
|
||||||
|
left: operand,
|
||||||
|
right: 0,
|
||||||
|
op: PolicyOp::Not,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_logical_block_start(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||||
|
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||||
|
Ok(Instruction::LogicalBlockStart {
|
||||||
|
mode,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_allof_next(params_text: &str) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap();
|
||||||
|
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||||
|
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||||
|
Ok(Instruction::AllOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_logical_block_end(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||||
|
Ok(Instruction::LogicalBlockEnd { mode, result })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_anyof_next(params_text: &str) -> Result<Instruction> {
|
||||||
|
let params = parse_params(params_text)?;
|
||||||
|
let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap();
|
||||||
|
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||||
|
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||||
|
Ok(Instruction::AnyOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1059,4 +1059,10 @@ mod tests {
|
|||||||
fn run_loop_test_file(file: &str) {
|
fn run_loop_test_file(file: &str) {
|
||||||
run_vm_test_suite(file).unwrap()
|
run_vm_test_suite(file).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "azure_policy")]
|
||||||
|
#[test_resources("tests/rvm/vm/suites/azure_policy/*.yaml")]
|
||||||
|
fn run_azure_policy_test_file(file: &str) {
|
||||||
|
run_vm_test_suite(file).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -807,6 +807,297 @@ impl RegoVM {
|
|||||||
let result = self.get_register(0)?.clone();
|
let result = self.get_register(0)?.clone();
|
||||||
Ok(InstructionOutcome::Return(result))
|
Ok(InstructionOutcome::Return(result))
|
||||||
}
|
}
|
||||||
|
other => self.execute_policy_instruction(program, other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "azure_policy"))]
|
||||||
|
fn execute_policy_instruction(
|
||||||
|
&mut self,
|
||||||
|
program: &Program,
|
||||||
|
instruction: Instruction,
|
||||||
|
) -> Result<InstructionOutcome> {
|
||||||
|
match instruction {
|
||||||
|
instruction @ (Instruction::PolicyCondition { .. }
|
||||||
|
| Instruction::LogicalBlockStart { .. }
|
||||||
|
| Instruction::LogicalBlockEnd { .. }
|
||||||
|
| Instruction::AllOfNext { .. }
|
||||||
|
| Instruction::AnyOfNext { .. }) => Err(VmError::UnhandledInstruction {
|
||||||
|
instruction: alloc::format!("{:?} requires the azure_policy feature", instruction),
|
||||||
|
pc: self.pc,
|
||||||
|
}),
|
||||||
|
other => self.execute_virtual_instruction(program, other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether `l` "contains" `r` using Azure Policy semantics.
|
||||||
|
///
|
||||||
|
/// Works on strings (case-insensitive substring), arrays/sets (element
|
||||||
|
/// membership), and objects (key membership). For string haystacks,
|
||||||
|
/// non-string scalar RHS values are coerced to strings before the
|
||||||
|
/// substring check. For non-string scalar LHS values, coercion to string
|
||||||
|
/// only happens when the RHS is already a string.
|
||||||
|
#[cfg(feature = "azure_policy")]
|
||||||
|
#[inline]
|
||||||
|
fn policy_contains_check(l: &Value, r: &Value) -> bool {
|
||||||
|
use crate::builtins::azure_policy::helpers::{case_insensitive_equals, coerce_to_string};
|
||||||
|
use crate::languages::azure_policy::strings;
|
||||||
|
|
||||||
|
match *l {
|
||||||
|
Value::String(ref haystack) => match *r {
|
||||||
|
Value::String(ref needle) => strings::case_fold::contains(haystack, needle),
|
||||||
|
_ => coerce_to_string(r)
|
||||||
|
.is_some_and(|needle| strings::case_fold::contains(haystack, &needle)),
|
||||||
|
},
|
||||||
|
Value::Array(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
|
||||||
|
Value::Set(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
|
||||||
|
// ARM template contains(object, key) checks key membership.
|
||||||
|
Value::Object(ref map) => map.keys().any(|key| case_insensitive_equals(key, r)),
|
||||||
|
// Coerce non-string scalar LHS (e.g., count result)
|
||||||
|
// to a string only when the RHS is already a string.
|
||||||
|
_ => {
|
||||||
|
if let Value::String(ref needle) = *r {
|
||||||
|
coerce_to_string(l)
|
||||||
|
.is_some_and(|haystack| strings::case_fold::contains(&haystack, needle))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a Policy comparison operator. Undefined LHS → false.
|
||||||
|
#[cfg(feature = "azure_policy")]
|
||||||
|
fn policy_compare(
|
||||||
|
&mut self,
|
||||||
|
dest: u8,
|
||||||
|
left: u8,
|
||||||
|
right: u8,
|
||||||
|
cmp: fn(i8) -> bool,
|
||||||
|
) -> Result<InstructionOutcome> {
|
||||||
|
use crate::builtins::azure_policy::helpers::{compare_values, is_undefined};
|
||||||
|
|
||||||
|
let l = self.get_register(left)?;
|
||||||
|
if is_undefined(l) {
|
||||||
|
self.set_register(dest, Value::Bool(false))?;
|
||||||
|
} else {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
let result = compare_values(l, r).is_some_and(cmp);
|
||||||
|
self.set_register(dest, Value::Bool(result))?;
|
||||||
|
}
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "azure_policy")]
|
||||||
|
fn execute_policy_instruction(
|
||||||
|
&mut self,
|
||||||
|
program: &Program,
|
||||||
|
instruction: Instruction,
|
||||||
|
) -> Result<InstructionOutcome> {
|
||||||
|
use crate::builtins::azure_policy::helpers::{
|
||||||
|
as_boolish, case_insensitive_equals, coerce_to_string_ci,
|
||||||
|
collection_any_ci_eq_excluding_null, collection_has_null, is_true, is_undefined,
|
||||||
|
match_like_pattern_ci, match_pattern,
|
||||||
|
};
|
||||||
|
use crate::rvm::instructions::{LogicalBlockMode, PolicyOp};
|
||||||
|
|
||||||
|
use Instruction::*;
|
||||||
|
match instruction {
|
||||||
|
PolicyCondition {
|
||||||
|
dest,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
op,
|
||||||
|
} => {
|
||||||
|
let l = self.get_register(left)?;
|
||||||
|
let result = match op {
|
||||||
|
PolicyOp::Equals => {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(l) {
|
||||||
|
matches!(r, Value::Null)
|
||||||
|
} else {
|
||||||
|
case_insensitive_equals(l, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::NotEquals => {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(l) {
|
||||||
|
!matches!(r, Value::Null)
|
||||||
|
} else {
|
||||||
|
!case_insensitive_equals(l, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Greater => {
|
||||||
|
return self.policy_compare(dest, left, right, |c| c > 0);
|
||||||
|
}
|
||||||
|
PolicyOp::GreaterOrEquals => {
|
||||||
|
return self.policy_compare(dest, left, right, |c| c >= 0);
|
||||||
|
}
|
||||||
|
PolicyOp::Less => {
|
||||||
|
return self.policy_compare(dest, left, right, |c| c < 0);
|
||||||
|
}
|
||||||
|
PolicyOp::LessOrEquals => {
|
||||||
|
return self.policy_compare(dest, left, right, |c| c <= 0);
|
||||||
|
}
|
||||||
|
PolicyOp::In => {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(l) {
|
||||||
|
collection_has_null(r)
|
||||||
|
} else if matches!(*l, Value::Null) || is_undefined(r) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
collection_any_ci_eq_excluding_null(r, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::NotIn => {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(l) {
|
||||||
|
!collection_has_null(r)
|
||||||
|
} else if matches!(*l, Value::Null) || is_undefined(r) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!collection_any_ci_eq_excluding_null(r, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Contains | PolicyOp::NotContains => {
|
||||||
|
let negated = op.is_negated();
|
||||||
|
if is_undefined(l) {
|
||||||
|
negated
|
||||||
|
} else {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(r) {
|
||||||
|
// undefined RHS: positive → false, negated → false
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
negated ^ Self::policy_contains_check(l, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::ContainsKey | PolicyOp::NotContainsKey => {
|
||||||
|
let negated = op.is_negated();
|
||||||
|
if is_undefined(l) {
|
||||||
|
negated
|
||||||
|
} else {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
if is_undefined(r) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let found = match *l {
|
||||||
|
Value::Object(ref map) => {
|
||||||
|
map.keys().any(|key| case_insensitive_equals(key, r))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
negated ^ found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Like | PolicyOp::NotLike => {
|
||||||
|
let negated = op.is_negated();
|
||||||
|
if is_undefined(l) {
|
||||||
|
negated
|
||||||
|
} else {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
let positive = match (coerce_to_string_ci(l), coerce_to_string_ci(r)) {
|
||||||
|
(Some(input), Some(pattern)) => {
|
||||||
|
match_like_pattern_ci(&input, &pattern)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
negated ^ positive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Match
|
||||||
|
| PolicyOp::NotMatch
|
||||||
|
| PolicyOp::MatchInsensitively
|
||||||
|
| PolicyOp::NotMatchInsensitively => {
|
||||||
|
let negated = op.is_negated();
|
||||||
|
let case_insensitive = matches!(
|
||||||
|
op,
|
||||||
|
PolicyOp::MatchInsensitively | PolicyOp::NotMatchInsensitively
|
||||||
|
);
|
||||||
|
if is_undefined(l) {
|
||||||
|
negated
|
||||||
|
} else {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
negated ^ match_pattern(l, r, case_insensitive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Exists => {
|
||||||
|
let r = self.get_register(right)?;
|
||||||
|
let expected = as_boolish(r).unwrap_or(false);
|
||||||
|
let is_defined = !is_undefined(l) && !matches!(l, Value::Null);
|
||||||
|
is_defined == expected
|
||||||
|
}
|
||||||
|
PolicyOp::ValueConditionGuard => {
|
||||||
|
// left = value register, right = condition register
|
||||||
|
if is_undefined(l) {
|
||||||
|
self.set_register(dest, Value::Bool(false))?;
|
||||||
|
return Ok(InstructionOutcome::Continue);
|
||||||
|
} else {
|
||||||
|
let c = self.get_register(right)?.clone();
|
||||||
|
self.set_register(dest, c)?;
|
||||||
|
return Ok(InstructionOutcome::Continue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PolicyOp::Not => {
|
||||||
|
// left = operand, right unused
|
||||||
|
!is_true(l)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.set_register(dest, Value::Bool(result))?;
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllOf / AnyOf structured instructions
|
||||||
|
LogicalBlockStart {
|
||||||
|
mode: _,
|
||||||
|
result,
|
||||||
|
end_pc: _,
|
||||||
|
} => {
|
||||||
|
// Initialize result to false (pessimistic).
|
||||||
|
self.set_register(result, Value::Bool(false))?;
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
AllOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
} => {
|
||||||
|
let val = self.get_register(check)?;
|
||||||
|
if !matches!(val, Value::Bool(true)) {
|
||||||
|
// Child failed — short-circuit. Ensure the block result is false.
|
||||||
|
self.set_register(result, Value::Bool(false))?;
|
||||||
|
self.pc = usize::from(end_pc);
|
||||||
|
}
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
AnyOfNext {
|
||||||
|
check,
|
||||||
|
result,
|
||||||
|
end_pc,
|
||||||
|
} => {
|
||||||
|
let val = self.get_register(check)?;
|
||||||
|
if matches!(val, Value::Bool(true)) {
|
||||||
|
// Child succeeded — short-circuit.
|
||||||
|
self.set_register(result, Value::Bool(true))?;
|
||||||
|
self.pc = usize::from(end_pc);
|
||||||
|
}
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
LogicalBlockEnd { mode, result } => {
|
||||||
|
match mode {
|
||||||
|
LogicalBlockMode::AllOf => {
|
||||||
|
// All children passed — set result to true.
|
||||||
|
self.set_register(result, Value::Bool(true))?;
|
||||||
|
}
|
||||||
|
LogicalBlockMode::AnyOf => {
|
||||||
|
// No child matched — result stays false (set by LogicalBlockStart).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(InstructionOutcome::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
other => self.execute_virtual_instruction(program, other),
|
other => self.execute_virtual_instruction(program, other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
680
tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml
Normal file
680
tests/rvm/vm/suites/azure_policy/policy_allof_anyof.yaml
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
# AllOf / AnyOf structured short-circuit instruction tests
|
||||||
|
#
|
||||||
|
# AllOf: conjunction with lazy short-circuit.
|
||||||
|
# AllOfStart — initialize result=false
|
||||||
|
# AllOfNext — if child is not true, set pc=end_pc; after the main-loop
|
||||||
|
# increment, execution resumes past AllOfEnd (result stays false)
|
||||||
|
# AllOfEnd — all children passed, set result=true
|
||||||
|
#
|
||||||
|
# AnyOf: disjunction with lazy short-circuit.
|
||||||
|
# AnyOfStart — initialize result=false
|
||||||
|
# AnyOfNext — if child is true, set result=true, set pc=end_pc; after the
|
||||||
|
# main-loop increment, execution resumes past AnyOfEnd
|
||||||
|
# AnyOfEnd — no child matched, result stays false
|
||||||
|
#
|
||||||
|
# PC semantics: end_pc points to the AllOfEnd/AnyOfEnd instruction.
|
||||||
|
# On short-circuit the VM sets self.pc = end_pc; because the main loop
|
||||||
|
# increments pc after Continue, the End instruction itself is skipped.
|
||||||
|
|
||||||
|
cases:
|
||||||
|
# =========================================================================
|
||||||
|
# AllOf basic cases
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: allof_all_true
|
||||||
|
description: "allOf with two true children → true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfStart — end_pc=5 points to AllOfEnd at PC 5
|
||||||
|
- "AllOfStart { result: 0, end_pc: 5 }"
|
||||||
|
# Child 1: evaluate condition into r1
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
# Child 2: evaluate condition into r2
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
# PC 5: AllOfEnd — all passed
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: allof_first_false
|
||||||
|
description: "allOf with first child false → short-circuits, result false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfStart — end_pc=5
|
||||||
|
- "AllOfStart { result: 0, end_pc: 5 }"
|
||||||
|
# Child 1: false
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
# Child 2: this should be skipped
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
# PC 5: AllOfEnd
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: allof_second_false
|
||||||
|
description: "allOf with second child false → result false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AllOfStart { result: 0, end_pc: 5 }"
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: allof_empty
|
||||||
|
description: "allOf with zero children → AllOfStart immediately followed by AllOfEnd → true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfStart — end_pc=1 points to AllOfEnd at PC 1
|
||||||
|
- "AllOfStart { result: 0, end_pc: 1 }"
|
||||||
|
# PC 1: AllOfEnd
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: allof_single_true
|
||||||
|
description: "allOf with single true child → true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AllOfStart { result: 0, end_pc: 3 }"
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: allof_single_false
|
||||||
|
description: "allOf with single false child → false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AllOfStart { result: 0, end_pc: 3 }"
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: allof_undefined_child
|
||||||
|
description: "allOf: first child undefined (not true), second child true → short-circuits to false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfEnd at PC 5
|
||||||
|
- "AllOfStart { result: 0, end_pc: 5 }"
|
||||||
|
# PC 1: r1 is undefined (not initialized) → AllOfNext short-circuits
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
# PC 2: Child 2 (skipped)
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 3 (skipped)
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
# PC 4: should not happen
|
||||||
|
- "LoadTrue { dest: 3 }"
|
||||||
|
# PC 5: AllOfEnd (skipped on short-circuit)
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 6
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# AnyOf basic cases
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: anyof_first_true
|
||||||
|
description: "anyOf with first child true → short-circuits, result true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AnyOfStart — end_pc=5
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 5 }"
|
||||||
|
# Child 1: true
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
# Child 2: should be skipped
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
# PC 5: AnyOfEnd
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: anyof_second_true
|
||||||
|
description: "anyOf with second child true → result true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 5 }"
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: anyof_all_false
|
||||||
|
description: "anyOf with all children false → result false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 5 }"
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: anyof_empty
|
||||||
|
description: "anyOf with zero children → false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 1 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: anyof_single_true
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 3 }"
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: anyof_single_false
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 3 }"
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Nested allOf inside anyOf
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: nested_allof_in_anyof
|
||||||
|
description: |
|
||||||
|
anyOf [
|
||||||
|
allOf [false, true], -- fails
|
||||||
|
allOf [true, true] -- passes → anyOf result is true
|
||||||
|
]
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# Outer anyOf: AnyOfEnd at PC 15
|
||||||
|
# PC 0
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 15 }"
|
||||||
|
|
||||||
|
# --- anyOf child 1: allOf [false, true] ---
|
||||||
|
# Inner allOf #1: AllOfEnd at PC 6
|
||||||
|
# PC 1
|
||||||
|
- "AllOfStart { result: 3, end_pc: 6 }"
|
||||||
|
# PC 2
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
# PC 3: short-circuits → pc=6, +1→7 (skips AllOfEnd)
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 6 }"
|
||||||
|
# PC 4 (skipped)
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 5 (skipped)
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 6 }"
|
||||||
|
# PC 6
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 7: AnyOfNext — r3=false → no short-circuit, continue
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
|
||||||
|
# --- anyOf child 2: allOf [true, true] ---
|
||||||
|
# Inner allOf #2: AllOfEnd at PC 13
|
||||||
|
# PC 8
|
||||||
|
- "AllOfStart { result: 3, end_pc: 13 }"
|
||||||
|
# PC 9
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 10
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 13 }"
|
||||||
|
# PC 11
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 12
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 13 }"
|
||||||
|
# PC 13
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 14: AnyOfNext — r3=true → short-circuit! r0=true, skip AnyOfEnd
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
# PC 15
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
# PC 16
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Nested anyOf inside allOf
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: nested_anyof_in_allof
|
||||||
|
description: |
|
||||||
|
allOf [
|
||||||
|
anyOf [false, true], -- passes (second child true)
|
||||||
|
anyOf [false, false] -- fails
|
||||||
|
] → false
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# Outer allOf: AllOfEnd at PC 15
|
||||||
|
# PC 0
|
||||||
|
- "AllOfStart { result: 0, end_pc: 15 }"
|
||||||
|
|
||||||
|
# --- allOf child 1: anyOf [false, true] ---
|
||||||
|
# Inner anyOf #1: AnyOfEnd at PC 6
|
||||||
|
# PC 1
|
||||||
|
- "AnyOfStart { result: 3, end_pc: 6 }"
|
||||||
|
# PC 2
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
# PC 3
|
||||||
|
- "AnyOfNext { check: 1, result: 3, end_pc: 6 }"
|
||||||
|
# PC 4
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 5: r2=true → short-circuit! r3=true, skip AnyOfEnd
|
||||||
|
- "AnyOfNext { check: 2, result: 3, end_pc: 6 }"
|
||||||
|
# PC 6
|
||||||
|
- "AnyOfEnd { result: 3 }"
|
||||||
|
# PC 7: AllOfNext — r3=true → no short-circuit, continue
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
|
||||||
|
# --- allOf child 2: anyOf [false, false] ---
|
||||||
|
# Inner anyOf #2: AnyOfEnd at PC 13
|
||||||
|
# PC 8
|
||||||
|
- "AnyOfStart { result: 3, end_pc: 13 }"
|
||||||
|
# PC 9
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
# PC 10
|
||||||
|
- "AnyOfNext { check: 1, result: 3, end_pc: 13 }"
|
||||||
|
# PC 11
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
# PC 12
|
||||||
|
- "AnyOfNext { check: 2, result: 3, end_pc: 13 }"
|
||||||
|
# PC 13
|
||||||
|
- "AnyOfEnd { result: 3 }"
|
||||||
|
# PC 14: AllOfNext — r3=false → short-circuit! skip AllOfEnd
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
# PC 15
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 16
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# AllOf/AnyOf with policy operators
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: allof_with_policy_equals
|
||||||
|
description: "allOf [ PolicyEquals(a,a), PolicyEquals(b,B) ] → true"
|
||||||
|
literals: ["hello", "hello", "World", "WORLD"]
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfEnd at PC 9
|
||||||
|
- "AllOfStart { result: 0, end_pc: 9 }"
|
||||||
|
# Child 1: PolicyEquals("hello", "hello") → r3=true
|
||||||
|
# PC 1
|
||||||
|
- "Load { dest: 1, literal_idx: 0 }"
|
||||||
|
# PC 2
|
||||||
|
- "Load { dest: 2, literal_idx: 1 }"
|
||||||
|
# PC 3
|
||||||
|
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
|
||||||
|
# PC 4
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# Child 2: PolicyEquals("World", "WORLD") → r3=true
|
||||||
|
# PC 5
|
||||||
|
- "Load { dest: 1, literal_idx: 2 }"
|
||||||
|
# PC 6
|
||||||
|
- "Load { dest: 2, literal_idx: 3 }"
|
||||||
|
# PC 7
|
||||||
|
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
|
||||||
|
# PC 8
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# PC 9
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 10
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: anyof_with_policy_operators
|
||||||
|
description: "anyOf [ PolicyEquals(a,b), PolicyContains(str,sub) ] → true via second child"
|
||||||
|
literals: ["hello", "world", "Hello World", "world"]
|
||||||
|
instructions:
|
||||||
|
# PC 0
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 9 }"
|
||||||
|
# Child 1: PolicyEquals("hello", "world") → false
|
||||||
|
- "Load { dest: 1, literal_idx: 0 }"
|
||||||
|
- "Load { dest: 2, literal_idx: 1 }"
|
||||||
|
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# Child 2: PolicyContains("Hello World", "world") → true
|
||||||
|
- "Load { dest: 1, literal_idx: 2 }"
|
||||||
|
- "Load { dest: 2, literal_idx: 3 }"
|
||||||
|
- "PolicyContains { dest: 3, left: 1, right: 2 }"
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# PC 9
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Three children
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: allof_three_children
|
||||||
|
description: "allOf with three true children"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AllOfStart { result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 3 }"
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: allof_third_false
|
||||||
|
description: "allOf with third child false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AllOfStart { result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadFalse { dest: 3 }"
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: anyof_third_true
|
||||||
|
description: "anyOf where only third child is true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 7 }"
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 7 }"
|
||||||
|
- "LoadTrue { dest: 3 }"
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 7 }"
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Additional corner cases
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
- note: anyof_undefined_child
|
||||||
|
description: "anyOf with undefined child (not true) — continues evaluation"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AnyOfEnd at PC 5
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 5 }"
|
||||||
|
# PC 1: r1 is undefined → AnyOfNext does not short-circuit
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 5 }"
|
||||||
|
# PC 2: Child 2 = true → short-circuit
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 3
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 5 }"
|
||||||
|
# PC 4: (skipped)
|
||||||
|
- "LoadFalse { dest: 3 }"
|
||||||
|
# PC 5
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
# PC 6
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: anyof_all_undefined
|
||||||
|
description: "anyOf with all children undefined → false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AnyOfEnd at PC 3
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 3 }"
|
||||||
|
# PC 1: r1 undefined
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
# PC 2: r2 undefined
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 3 }"
|
||||||
|
# PC 3
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
# PC 4
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: allof_all_undefined
|
||||||
|
description: "allOf with all children undefined → false (short-circuits on first)"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfEnd at PC 3
|
||||||
|
- "AllOfStart { result: 0, end_pc: 3 }"
|
||||||
|
# PC 1: r1 undefined → short-circuit
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 3 }"
|
||||||
|
# PC 2: r2 undefined (skipped)
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 3 }"
|
||||||
|
# PC 3
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 4
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: allof_middle_false
|
||||||
|
description: "allOf with 3 children, only middle fails → false"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfEnd at PC 7
|
||||||
|
- "AllOfStart { result: 0, end_pc: 7 }"
|
||||||
|
# PC 1
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 2
|
||||||
|
- "AllOfNext { check: 1, result: 0, end_pc: 7 }"
|
||||||
|
# PC 3: middle child = false
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
# PC 4: short-circuits
|
||||||
|
- "AllOfNext { check: 2, result: 0, end_pc: 7 }"
|
||||||
|
# PC 5 (skipped)
|
||||||
|
- "LoadTrue { dest: 3 }"
|
||||||
|
# PC 6 (skipped)
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 7 }"
|
||||||
|
# PC 7 (skipped)
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 8
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: anyof_middle_true
|
||||||
|
description: "anyOf with 3 children, only middle is true → true"
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# PC 0: AnyOfEnd at PC 7
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 7 }"
|
||||||
|
# PC 1
|
||||||
|
- "LoadFalse { dest: 1 }"
|
||||||
|
# PC 2
|
||||||
|
- "AnyOfNext { check: 1, result: 0, end_pc: 7 }"
|
||||||
|
# PC 3: middle child = true → short-circuit
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 4
|
||||||
|
- "AnyOfNext { check: 2, result: 0, end_pc: 7 }"
|
||||||
|
# PC 5 (skipped)
|
||||||
|
- "LoadFalse { dest: 3 }"
|
||||||
|
# PC 6 (skipped)
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 7 }"
|
||||||
|
# PC 7 (skipped)
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
# PC 8
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: nested_allof_both_inner_pass
|
||||||
|
description: |
|
||||||
|
anyOf [
|
||||||
|
allOf [true, true], -- passes → anyOf short-circuits
|
||||||
|
allOf [true, true] -- skipped
|
||||||
|
] → true (verify short-circuit on first passing inner)
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# Outer anyOf: AnyOfEnd at PC 15
|
||||||
|
# PC 0
|
||||||
|
- "AnyOfStart { result: 0, end_pc: 15 }"
|
||||||
|
# Inner allOf #1: AllOfEnd at PC 6
|
||||||
|
# PC 1
|
||||||
|
- "AllOfStart { result: 3, end_pc: 6 }"
|
||||||
|
# PC 2
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 3
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 6 }"
|
||||||
|
# PC 4
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 5
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 6 }"
|
||||||
|
# PC 6
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 7: r3=true → short-circuit! → skip to PC 16
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
# Inner allOf #2 (ALL SKIPPED)
|
||||||
|
# PC 8
|
||||||
|
- "AllOfStart { result: 3, end_pc: 13 }"
|
||||||
|
# PC 9
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 10
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 13 }"
|
||||||
|
# PC 11
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 12
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 13 }"
|
||||||
|
# PC 13
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 14
|
||||||
|
- "AnyOfNext { check: 3, result: 0, end_pc: 15 }"
|
||||||
|
# PC 15
|
||||||
|
- "AnyOfEnd { result: 0 }"
|
||||||
|
# PC 16
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: register_isolation
|
||||||
|
description: |
|
||||||
|
allOf(r0) [ anyOf(r3) [true] ] → true.
|
||||||
|
Inner anyOf uses r3, outer allOf uses r0.
|
||||||
|
Verifies different result registers don't interfere.
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# Outer allOf: r0, AllOfEnd at PC 6
|
||||||
|
# PC 0
|
||||||
|
- "AllOfStart { result: 0, end_pc: 6 }"
|
||||||
|
# Inner anyOf: r3, AnyOfEnd at PC 4
|
||||||
|
# PC 1
|
||||||
|
- "AnyOfStart { result: 3, end_pc: 4 }"
|
||||||
|
# PC 2
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 3: r1=true → r3=true, pc=4, +1→5 (skip AnyOfEnd)
|
||||||
|
- "AnyOfNext { check: 1, result: 3, end_pc: 4 }"
|
||||||
|
# PC 4
|
||||||
|
- "AnyOfEnd { result: 3 }"
|
||||||
|
# PC 5: AllOfNext — r3=true → continue
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 6 }"
|
||||||
|
# PC 6
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 7
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
|
|
||||||
|
- note: allof_with_policy_eq_first_fails
|
||||||
|
description: "allOf [ PolicyEquals(a,b), PolicyEquals(c,c) ] → false (first child fails)"
|
||||||
|
literals: ["hello", "world", "same", "SAME"]
|
||||||
|
instructions:
|
||||||
|
# PC 0: AllOfEnd at PC 9
|
||||||
|
- "AllOfStart { result: 0, end_pc: 9 }"
|
||||||
|
# Child 1: PolicyEquals("hello", "world") → r3=false
|
||||||
|
# PC 1
|
||||||
|
- "Load { dest: 1, literal_idx: 0 }"
|
||||||
|
# PC 2
|
||||||
|
- "Load { dest: 2, literal_idx: 1 }"
|
||||||
|
# PC 3
|
||||||
|
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
|
||||||
|
# PC 4: short-circuits → skip AllOfEnd
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# Child 2 (skipped)
|
||||||
|
# PC 5
|
||||||
|
- "Load { dest: 1, literal_idx: 2 }"
|
||||||
|
# PC 6
|
||||||
|
- "Load { dest: 2, literal_idx: 3 }"
|
||||||
|
# PC 7
|
||||||
|
- "PolicyEquals { dest: 3, left: 1, right: 2 }"
|
||||||
|
# PC 8
|
||||||
|
- "AllOfNext { check: 3, result: 0, end_pc: 9 }"
|
||||||
|
# PC 9
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 10
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: false
|
||||||
|
|
||||||
|
- note: nested_3_deep
|
||||||
|
description: |
|
||||||
|
allOf [
|
||||||
|
anyOf [
|
||||||
|
allOf [true, false], -- inner allOf fails
|
||||||
|
allOf [true, true] -- inner allOf passes → anyOf passes
|
||||||
|
]
|
||||||
|
] → true
|
||||||
|
literals: []
|
||||||
|
instructions:
|
||||||
|
# Outer allOf: AllOfEnd at PC 18
|
||||||
|
# PC 0
|
||||||
|
- "AllOfStart { result: 0, end_pc: 18 }"
|
||||||
|
|
||||||
|
# Middle anyOf: AnyOfEnd at PC 16
|
||||||
|
# PC 1
|
||||||
|
- "AnyOfStart { result: 4, end_pc: 16 }"
|
||||||
|
|
||||||
|
# Inner allOf #1: [true, false] — AllOfEnd at PC 7
|
||||||
|
# PC 2
|
||||||
|
- "AllOfStart { result: 3, end_pc: 7 }"
|
||||||
|
# PC 3
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 4
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 7 }"
|
||||||
|
# PC 5
|
||||||
|
- "LoadFalse { dest: 2 }"
|
||||||
|
# PC 6: r2=false → short-circuit → pc=7, +1→8
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 7 }"
|
||||||
|
# PC 7: AllOfEnd (skipped when short-circuit, r3 stays false)
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 8: AnyOfNext — r3=false → no short-circuit, continue
|
||||||
|
- "AnyOfNext { check: 3, result: 4, end_pc: 16 }"
|
||||||
|
|
||||||
|
# Inner allOf #2: [true, true] — AllOfEnd at PC 14
|
||||||
|
# PC 9
|
||||||
|
- "AllOfStart { result: 3, end_pc: 14 }"
|
||||||
|
# PC 10
|
||||||
|
- "LoadTrue { dest: 1 }"
|
||||||
|
# PC 11
|
||||||
|
- "AllOfNext { check: 1, result: 3, end_pc: 14 }"
|
||||||
|
# PC 12
|
||||||
|
- "LoadTrue { dest: 2 }"
|
||||||
|
# PC 13
|
||||||
|
- "AllOfNext { check: 2, result: 3, end_pc: 14 }"
|
||||||
|
# PC 14: AllOfEnd — r3=true
|
||||||
|
- "AllOfEnd { result: 3 }"
|
||||||
|
# PC 15: AnyOfNext — r3=true → r4=true, pc=16, +1→17
|
||||||
|
- "AnyOfNext { check: 3, result: 4, end_pc: 16 }"
|
||||||
|
|
||||||
|
# PC 16: AnyOfEnd (skipped on short-circuit)
|
||||||
|
- "AnyOfEnd { result: 4 }"
|
||||||
|
# PC 17: AllOfNext — r4=true → continue
|
||||||
|
- "AllOfNext { check: 4, result: 0, end_pc: 18 }"
|
||||||
|
# PC 18: AllOfEnd — r0=true
|
||||||
|
- "AllOfEnd { result: 0 }"
|
||||||
|
# PC 19
|
||||||
|
- "Return { value: 0 }"
|
||||||
|
want_result: true
|
||||||
1583
tests/rvm/vm/suites/azure_policy/policy_operators.yaml
Normal file
1583
tests/rvm/vm/suites/azure_policy/policy_operators.yaml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user