mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat: add Azure Policy builtins with YAML test suite (#630)
* feat: add Azure Policy builtins with YAML test suite Implement ARM template functions for Azure Policy evaluation: Builtins: - String: indexOf, lastIndexOf, trim, format, split, startsWith, endsWith, padLeft, concat, replace, toLower, toUpper, substring, guid, uniqueString - DateTime: dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, addDays - Collection: intersection, union, take, skip, first, last, min, max, range, items, tryGet, tryIndexFromEnd, empty, array, createObject - Encoding: base64, base64ToString, base64ToJson, uri, uriComponent, uriComponentToString, dataUri, dataUriToString - Numeric: int, float, intDiv, intMod - Misc: json, join, bool, string, coalesce, if, getParameter, resolveField - Logic: logicAll, logicAny Key implementation details: - Unicode case-insensitive search via ICU4X case folding with single-pass fold_with_char_map() for indexOf/lastIndexOf - .NET composite formatting (System.String.Format) with alignment, standard and custom datetime format specifiers, numeric format specifiers - DateTime round-trip preserves input shape (Z vs +00:00, T vs space, fractional seconds) when no explicit output format is supplied - Zero-cost as_str() helper borrows directly from Value::String(Rc<str>) - BTreeSet<&Value> in array union avoids redundant cloning Test suite: - 53 YAML test files exercising all builtins via direct BUILTINS registry - Coverage for edge cases: empty inputs, Unicode, fractional seconds, invalid alignment, unknown format specifiers, RFC3339 offset shapes Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix: address PR review comments - Fix percent_encode to only uppercase hex digits, not entire string - Remove guid/uniqueString (unsupported); delete custom SHA-1 impl - Replace unwrap_or(0) with proper error in format placeholder parsing - Hoist CaseMapper into static CaseMapperBorrowed for zero per-call overhead - Pre-allocate Vec in range() with_capacity - Update bindings/ffi and bindings/ruby Cargo.lock - Fix uri_component test expectations for correct case preservation Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix: address second round of PR review comments - float(): return Undefined when as_f64() fails instead of leaking the original non-f64 representation - createObject(): reject odd number of arguments with an error (ARM-template parity) - format(): error on unknown numeric format specifiers instead of silently passing through (matches .NET FormatException behavior) - format(): cap alignment width at 10,000 to prevent DoS from user-controlled format strings like {0,1000000000} - Add YAML test cases for all new error behaviors Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix: address third round of PR review comments - percent_decode: reject incomplete % escapes (e.g. "%", "%2") instead of treating them as literal characters - parse_iso8601_duration: reject leftover digits without a unit designator at T boundary and end-of-input (e.g. "P1", "P1T2H") - yaml_to_value: panic on unsupported YAML numeric representations instead of silently mapping to Null - Revert unused src/languages/mod.rs changes (module is defined inline in lib.rs) Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix: add missing edge-case tests and fix empty-delimiter panic - fn_split: return input as single-element array for empty string delimiter instead of panicking (Rust's str::split("") panics) - format: add test for F3 higher precision ({0:F3} + 1.23456 → 1.235) - format: add test for N2 float with thousands separator - format: add test for negative index error ({-1}) - split: add test for empty-string delimiter - uri: add tests for query string and fragment in relative URI - createObject: add test for non-string (numeric) keys Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * fix: address fourth round of PR review comments - Add MAX_VARIADIC_ARGS (64) constant for variadic builtin arity instead of registering with 0 (logic_all, logic_any, min, max, format, intersection, union, coalesce, createObject); set dateTimeAdd to exact arity 3 - Switch indexOf/lastIndexOf to UTF-16 code-unit indices to match .NET String.IndexOf semantics (track ch.len_utf16() in fold_with_char_map, use encode_utf16().count() for empty-needle lastIndexOf) - Use DateTime::<Utc>::from_timestamp for explicit timezone type - Remove stale docs/azure-policy/casing.md link from module doc - Fix misleading comment in want_error test branch (code bails on Undefined, not accepts it) Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
f69974dc1b
commit
5b60daabd9
6
src/languages/azure_policy/mod.rs
Normal file
6
src/languages/azure_policy/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Azure Policy language support.
|
||||
|
||||
pub mod strings;
|
||||
223
src/languages/azure_policy/strings/case_fold.rs
Normal file
223
src/languages/azure_policy/strings/case_fold.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Full Unicode case folding for Azure Policy string value comparisons.
|
||||
//!
|
||||
//! Azure Policy evaluates most condition operators (`equals`, `notEquals`,
|
||||
//! `contains`, `like`, `in`, …) using .NET's `InvariantCultureIgnoreCase`.
|
||||
//! Under .NET 5+ this is backed by ICU, performing *full* Unicode case folding:
|
||||
//!
|
||||
//! - ß → ss (German sharp s)
|
||||
//! - ffi → ffi (Latin ligatures)
|
||||
//! - Σ/σ/ς → σ (Greek sigma variants)
|
||||
//!
|
||||
//! We use [`icu_casemap::CaseMapper`] with compiled data, which provides the
|
||||
//! same ICU case folding tables that .NET uses internally.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use regorus::languages::azure_policy::strings::case_fold;
|
||||
//!
|
||||
//! // Equality
|
||||
//! assert!(case_fold::eq("Straße", "STRASSE"));
|
||||
//! assert!(case_fold::eq("hello", "HELLO"));
|
||||
//!
|
||||
//! // Ordering
|
||||
//! assert_eq!(case_fold::cmp("alpha", "BETA"), std::cmp::Ordering::Less);
|
||||
//!
|
||||
//! // Folded form (for hashing, indexing, etc.)
|
||||
//! let folded: std::borrow::Cow<str> = case_fold::fold("Straße");
|
||||
//! assert_eq!(&*folded, "strasse");
|
||||
//! ```
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use icu_casemap::CaseMapperBorrowed;
|
||||
|
||||
static CASE_MAPPER: CaseMapperBorrowed<'static> = CaseMapperBorrowed::new();
|
||||
|
||||
/// Return the full-case-folded form of `s`.
|
||||
///
|
||||
/// The result is a [`Cow::Borrowed`] when `s` is already in folded form
|
||||
/// (common for ASCII lowercase), avoiding allocation.
|
||||
///
|
||||
/// This is the canonical form for case-insensitive hashing and indexing.
|
||||
pub fn fold(s: &str) -> Cow<'_, str> {
|
||||
CASE_MAPPER.fold_string(s)
|
||||
}
|
||||
|
||||
/// Case-insensitive equality using full Unicode case folding.
|
||||
///
|
||||
/// Equivalent to .NET `String.Equals(a, b, StringComparison.InvariantCultureIgnoreCase)`.
|
||||
///
|
||||
/// # Fast path
|
||||
///
|
||||
/// If both strings are plain ASCII, falls back to `eq_ignore_ascii_case`
|
||||
/// which avoids allocation entirely.
|
||||
pub fn eq(a: &str, b: &str) -> bool {
|
||||
// Fast path: ASCII-only strings are extremely common in Azure data.
|
||||
if a.is_ascii() && b.is_ascii() {
|
||||
return a.eq_ignore_ascii_case(b);
|
||||
}
|
||||
|
||||
CASE_MAPPER.fold_string(a) == CASE_MAPPER.fold_string(b)
|
||||
}
|
||||
|
||||
/// Case-insensitive ordering using full Unicode case folding.
|
||||
///
|
||||
/// Equivalent to .NET `String.Compare(a, b, StringComparison.InvariantCultureIgnoreCase)`.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This compares the *folded* UTF-8 byte sequences, which gives a stable
|
||||
/// total order suitable for sorting and binary search, but is not the same
|
||||
/// as a full locale-aware collation. For Azure Policy's purposes (where
|
||||
/// ordering is used by `greater`/`less` operators on string values) this is
|
||||
/// sufficient.
|
||||
pub fn cmp(a: &str, second: &str) -> Ordering {
|
||||
if a.is_ascii() && second.is_ascii() {
|
||||
// Compare char-by-char with ASCII folding, no allocation.
|
||||
let ord = a
|
||||
.bytes()
|
||||
.map(|byte| byte.to_ascii_lowercase())
|
||||
.cmp(second.bytes().map(|byte| byte.to_ascii_lowercase()));
|
||||
return ord;
|
||||
}
|
||||
|
||||
let fa = CASE_MAPPER.fold_string(a);
|
||||
let fb = CASE_MAPPER.fold_string(second);
|
||||
fa.cmp(&fb)
|
||||
}
|
||||
|
||||
/// Case-insensitive substring test using full Unicode case folding.
|
||||
///
|
||||
/// Returns `true` if the folded form of `haystack` contains the folded form
|
||||
/// of `needle`.
|
||||
///
|
||||
/// Equivalent to the `contains` / `notContains` Azure Policy operators on
|
||||
/// string values.
|
||||
pub fn contains(haystack: &str, needle: &str) -> bool {
|
||||
if haystack.is_ascii() && needle.is_ascii() {
|
||||
// ASCII fast path: avoid allocation.
|
||||
// Note: This is O(n*m) but strings are typically short in policy data.
|
||||
let h = haystack.as_bytes();
|
||||
let n = needle.as_bytes();
|
||||
if n.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if n.len() > h.len() {
|
||||
return false;
|
||||
}
|
||||
return h.windows(n.len()).any(|w| w.eq_ignore_ascii_case(n));
|
||||
}
|
||||
|
||||
let fh = CASE_MAPPER.fold_string(haystack);
|
||||
let fn_ = CASE_MAPPER.fold_string(needle);
|
||||
fh.contains(&*fn_)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── eq ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn eq_ascii_same_case() {
|
||||
assert!(eq("hello", "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_ascii_diff_case() {
|
||||
assert!(eq("Hello", "hELLO"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_sharp_s() {
|
||||
// ß (U+00DF) folds to "ss"
|
||||
assert!(eq("Straße", "STRASSE"));
|
||||
assert!(eq("straße", "strasse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_greek_sigma() {
|
||||
// Σ, σ, ς all fold to σ
|
||||
assert!(eq("ΣΕΛΑΣ", "σελας"));
|
||||
assert!(eq("σελας", "σελας"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_ligature() {
|
||||
// ffi (U+FB03) folds to "ffi"
|
||||
assert!(eq("ffi", "ffi"));
|
||||
assert!(eq("ffi", "FFI"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_not_equal() {
|
||||
assert!(!eq("abc", "abd"));
|
||||
assert!(!eq("abc", "ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_empty() {
|
||||
assert!(eq("", ""));
|
||||
assert!(!eq("", "a"));
|
||||
}
|
||||
|
||||
// ── cmp ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cmp_equal() {
|
||||
assert_eq!(cmp("hello", "HELLO"), Ordering::Equal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_less() {
|
||||
assert_eq!(cmp("alpha", "BETA"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_greater() {
|
||||
assert_eq!(cmp("beta", "ALPHA"), Ordering::Greater);
|
||||
}
|
||||
|
||||
// ── contains ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn contains_ascii() {
|
||||
assert!(contains("Hello World", "lo wo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_empty_needle() {
|
||||
assert!(contains("anything", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_sharp_s() {
|
||||
assert!(contains("Die Straße ist lang", "STRASSE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_no_match() {
|
||||
assert!(!contains("hello", "xyz"));
|
||||
}
|
||||
|
||||
// ── fold ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn fold_ascii_lowercase_borrows() {
|
||||
let result = fold("hello");
|
||||
assert!(matches!(result, Cow::Borrowed(_)));
|
||||
assert_eq!(&*result, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fold_sharp_s() {
|
||||
let result = fold("Straße");
|
||||
assert_eq!(&*result, "strasse");
|
||||
}
|
||||
}
|
||||
86
src/languages/azure_policy/strings/keys.rs
Normal file
86
src/languages/azure_policy/strings/keys.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! ASCII case-insensitive comparison for ARM object property keys.
|
||||
//!
|
||||
//! ARM resource property names are restricted to ASCII letters, digits, and
|
||||
//! underscores. Key lookup in normalized ARM objects therefore only needs
|
||||
//! `OrdinalIgnoreCase` semantics (fold A-Z → a-z, nothing else).
|
||||
//!
|
||||
//! This module wraps `eq_ignore_ascii_case` and provides an ASCII-only
|
||||
//! case-insensitive ordering, both zero-allocation and branchless on modern
|
||||
//! hardware.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use regorus::languages::azure_policy::strings::keys;
|
||||
//!
|
||||
//! assert!(keys::eq("Location", "location"));
|
||||
//! assert_eq!(keys::cmp("Name", "name"), std::cmp::Ordering::Equal);
|
||||
//! ```
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
/// ASCII case-insensitive equality for ARM property keys.
|
||||
///
|
||||
/// This is a thin wrapper around [`str::eq_ignore_ascii_case`], made explicit
|
||||
/// to document the `OrdinalIgnoreCase` semantics and keep call sites readable.
|
||||
#[inline]
|
||||
pub const fn eq(a: &str, b: &str) -> bool {
|
||||
a.eq_ignore_ascii_case(b)
|
||||
}
|
||||
|
||||
/// ASCII case-insensitive ordering for ARM property keys.
|
||||
///
|
||||
/// Compares byte-by-byte after folding each byte with
|
||||
/// [`u8::to_ascii_lowercase`]. This gives a stable total order consistent
|
||||
/// with [`eq`].
|
||||
#[inline]
|
||||
pub fn cmp(a: &str, second: &str) -> Ordering {
|
||||
a.bytes()
|
||||
.map(|byte| byte.to_ascii_lowercase())
|
||||
.cmp(second.bytes().map(|byte| byte.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn eq_same() {
|
||||
assert!(eq("location", "location"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_diff_case() {
|
||||
assert!(eq("Location", "LOCATION"));
|
||||
assert!(eq("apiVersion", "APIversion"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_not_equal() {
|
||||
assert!(!eq("name", "type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_empty() {
|
||||
assert!(eq("", ""));
|
||||
assert!(!eq("", "a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_equal() {
|
||||
assert_eq!(cmp("Location", "location"), Ordering::Equal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_less() {
|
||||
assert_eq!(cmp("apiVersion", "Name"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_greater() {
|
||||
assert_eq!(cmp("type", "Name"), Ordering::Greater);
|
||||
}
|
||||
}
|
||||
28
src/languages/azure_policy/strings/mod.rs
Normal file
28
src/languages/azure_policy/strings/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! String operations for ARM and Azure Policy.
|
||||
//!
|
||||
//! This module provides precise implementations of the string comparison
|
||||
//! semantics used by ARM (Azure Resource Manager) and Azure Policy:
|
||||
//!
|
||||
//! - **[`keys`]**: ASCII case-insensitive comparison for ARM object property
|
||||
//! keys (`OrdinalIgnoreCase` — only folds A-Z ↔ a-z).
|
||||
//!
|
||||
//! - **[`case_fold`]**: Full Unicode case folding for Azure Policy condition
|
||||
//! value comparisons (`InvariantCultureIgnoreCase`). Backed by ICU4X
|
||||
//! [`icu_casemap`] so that ß = SS, ffi = FFI, etc.
|
||||
//!
|
||||
//! # Design rationale
|
||||
//!
|
||||
//! ARM property key names are restricted to ASCII (letters, digits,
|
||||
//! underscores), so `eq_ignore_ascii_case` is both correct and fast for key
|
||||
//! lookup.
|
||||
//!
|
||||
//! Azure Policy condition operators (`equals`, `contains`, `like`, …) compare
|
||||
//! string *values* using .NET's `InvariantCultureIgnoreCase`, which performs
|
||||
//! full Unicode case folding. We use ICU4X's `CaseMapper::fold` / `fold_string`
|
||||
//! for this — it is the same ICU backing that .NET 5+ uses internally.
|
||||
|
||||
pub mod case_fold;
|
||||
pub mod keys;
|
||||
Reference in New Issue
Block a user