Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions bindings/csharp/Regorus.Tests/RegorusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ public void Rule_conflict_preserves_error_status_and_reports_previous_location()
Assert.IsFalse(ex.Message.Contains('"'));
}

[TestMethod]
public void Dotted_package_metadata_and_rule_paths_remain_canonical()
{
using var engine = new Engine();
var package = engine.AddPolicy(
"namespace.rego",
"""
package graph.defUniqueName["1.0.0"]

default deny := false
deny := true if { input.blocked == true }
""");

Assert.AreEqual("data.graph.defUniqueName[\"1.0.0\"]", package);
engine.SetInputJson("""{"blocked":false}""");
Assert.AreEqual(
"false",
engine.EvalRule("data.graph.defUniqueName[\"1.0.0\"].deny"));
}

[TestMethod]
public void Evaluation_using_file_policies_succeeds()
{
Expand Down
283 changes: 283 additions & 0 deletions bindings/ffi/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,286 @@ pub extern "C" fn regorus_engine_compile_program_with_entrypoints(
}
})
}

#[cfg(all(test, feature = "std"))]
mod namespace_tests {
use super::{
regorus_engine_add_policy, regorus_engine_compile_program_with_entrypoints,
regorus_engine_drop, regorus_engine_eval_rule, regorus_engine_new,
};
use crate::common::{regorus_result_drop, RegorusStatus};
use core::ffi::CStr;
use std::ffi::CString;

#[test]
fn canonical_dotted_namespace_paths_cross_the_ffi_boundary() {
let engine = regorus_engine_new();
assert!(!engine.is_null());

let file = CString::new("namespace.rego").expect("valid file path");
let policy = CString::new(
"package graph.defUniqueName[\"1.0.0\"]\n\
default deny := false\n\
deny := true if { input.blocked == true }\n",
)
.expect("valid policy");
let added = regorus_engine_add_policy(engine, file.as_ptr(), policy.as_ptr());
assert!(matches!(added.status, RegorusStatus::Ok));
unsafe {
let package = CStr::from_ptr(added.output)
.to_str()
.expect("UTF-8 package");
assert_eq!(package, "data.graph.defUniqueName[\"1.0.0\"]");
}
regorus_result_drop(added);

let rule =
CString::new("data.graph.defUniqueName[\"1.0.0\"].deny").expect("valid rule path");
let result = regorus_engine_eval_rule(engine, rule.as_ptr());
assert!(matches!(result.status, RegorusStatus::Ok));
unsafe {
let value = CStr::from_ptr(result.output).to_str().expect("UTF-8 value");
assert_eq!(value, "false");
}
regorus_result_drop(result);
regorus_engine_drop(engine);
}

#[test]
fn deep_bracketed_registered_path_matches_dotted_path_and_reuses_ffi_engine() {
let _poison_test_lock = crate::panic_guard::lock_poison_test_state();
crate::panic_guard::reset_poison();

let engine = regorus_engine_new();
assert!(!engine.is_null());

let package_components = (0..31).map(|index| format!("p{index}")).collect::<Vec<_>>();
let package_path = package_components.join(".");
let dotted_path = format!("data.{package_path}.value");
let mut bracketed_path = String::from("data");
for component in &package_components {
bracketed_path.push_str(&format!("[\"{component}\"]"));
}
bracketed_path.push_str("[\"value\"]");

let file = CString::new("deep.rego").expect("valid policy path");
let policy =
CString::new(format!("package {package_path}\nvalue := 7")).expect("valid policy");
let added = regorus_engine_add_policy(engine, file.as_ptr(), policy.as_ptr());
assert!(matches!(&added.status, RegorusStatus::Ok));
regorus_result_drop(added);

let dotted = CString::new(dotted_path).expect("valid dotted path");
let dotted_result = regorus_engine_eval_rule(engine, dotted.as_ptr());
assert!(matches!(&dotted_result.status, RegorusStatus::Ok));
assert_eq!(
unsafe { CStr::from_ptr(dotted_result.output) }
.to_str()
.expect("UTF-8 result"),
"7"
);
regorus_result_drop(dotted_result);

let bracketed = CString::new(bracketed_path.clone()).expect("valid bracketed path");
let bracketed_result = regorus_engine_eval_rule(engine, bracketed.as_ptr());
assert!(
matches!(&bracketed_result.status, RegorusStatus::Ok),
"equivalent bracketed lookup failed: {:?}",
bracketed_result.status
);
assert_eq!(
unsafe { CStr::from_ptr(bracketed_result.output) }
.to_str()
.expect("UTF-8 result"),
"7"
);
regorus_result_drop(bracketed_result);

let missing_path = bracketed_path.replace("[\"value\"]", "[\"missing\"]");
let missing = CString::new(missing_path).expect("valid missing path");
let rejected = regorus_engine_eval_rule(engine, missing.as_ptr());
assert!(matches!(&rejected.status, RegorusStatus::Error));
regorus_result_drop(rejected);
assert!(
!crate::panic_guard::is_poisoned(),
"a lookup error must not poison the FFI engine"
);

let reused_result = regorus_engine_eval_rule(engine, bracketed.as_ptr());
assert!(matches!(&reused_result.status, RegorusStatus::Ok));
assert_eq!(
unsafe { CStr::from_ptr(reused_result.output) }
.to_str()
.expect("UTF-8 result"),
"7"
);
regorus_result_drop(reused_result);
regorus_engine_drop(engine);
}

#[test]
fn overdeep_policy_path_returns_an_error_and_keeps_the_engine_usable() {
let engine = regorus_engine_new();
assert!(!engine.is_null());

let stable_file = CString::new("stable.rego").expect("valid file path");
let stable_policy = CString::new("package stable\nvalue := true").expect("valid policy");
let added = regorus_engine_add_policy(engine, stable_file.as_ptr(), stable_policy.as_ptr());
assert!(matches!(&added.status, RegorusStatus::Ok));
regorus_result_drop(added);

let deep_package = ["segment"; 33].join(".");
let deep_file = CString::new("deep.rego").expect("valid file path");
let deep_policy =
CString::new(format!("package {deep_package}\nvalue := true")).expect("valid policy");
let rejected = regorus_engine_add_policy(engine, deep_file.as_ptr(), deep_policy.as_ptr());
let is_error = matches!(&rejected.status, RegorusStatus::Error);
let error_message = if rejected.error_message.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr(rejected.error_message) }
.to_str()
.expect("UTF-8 error message")
.to_owned(),
)
};
regorus_result_drop(rejected);
assert!(
is_error,
"overdeep policy was not rejected: {error_message:?}"
);

let query = CString::new("data.stable.value").expect("valid rule path");
let result = regorus_engine_eval_rule(engine, query.as_ptr());
let succeeded = matches!(&result.status, RegorusStatus::Ok);
let output = if result.output.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr(result.output) }
.to_str()
.expect("UTF-8 result")
.to_owned(),
)
};
regorus_result_drop(result);
assert!(
succeeded,
"engine failed after rejecting the policy: {output:?}"
);
assert_eq!(output.as_deref(), Some("true"));

let long_dotted = format!("data{}", ".a".repeat(300_000));
let long_bracketed = format!("data{}", r#"["a"]"#.repeat(180_000));
for path in [long_dotted, long_bracketed] {
assert!(path.len() < 1024 * 1024);
let rule = CString::new(path).expect("valid rule path");
let rejected = regorus_engine_eval_rule(engine, rule.as_ptr());
let is_error = matches!(&rejected.status, RegorusStatus::Error);
let error_message = if rejected.error_message.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr(rejected.error_message) }
.to_str()
.expect("UTF-8 error message")
.to_owned(),
)
};
regorus_result_drop(rejected);
assert!(
is_error,
"huge rule path was not rejected: {error_message:?}"
);
assert_eq!(
error_message.as_deref(),
Some("not a valid rule path"),
"expected the path validation error, not an engine-poisoned error"
);

let result = regorus_engine_eval_rule(engine, query.as_ptr());
let succeeded = matches!(&result.status, RegorusStatus::Ok);
let output = if result.output.is_null() {
None
} else {
Some(
unsafe { CStr::from_ptr(result.output) }
.to_str()
.expect("UTF-8 result")
.to_owned(),
)
};
regorus_result_drop(result);
assert!(
succeeded,
"engine failed after rejecting the rule: {output:?}"
);
assert_eq!(output.as_deref(), Some("true"));
}

regorus_engine_drop(engine);
}

#[cfg(feature = "rvm")]
#[test]
fn bracketed_identifier_entrypoint_compiles_and_executes_through_engine_ffi() {
use crate::rvm::{
regorus_program_drop, regorus_rvm_drop, regorus_rvm_execute_entry_point_by_index,
regorus_rvm_execute_entry_point_by_name, regorus_rvm_load_program, regorus_rvm_new,
RegorusProgram,
};

let engine = regorus_engine_new();
assert!(!engine.is_null());

let file = CString::new("namespace.rego").expect("valid file path");
let policy = CString::new("package graph.version\nvalue := 7").expect("valid policy");
let added = regorus_engine_add_policy(engine, file.as_ptr(), policy.as_ptr());
assert!(matches!(added.status, RegorusStatus::Ok));
regorus_result_drop(added);

let entrypoint = CString::new("data.graph[\"version\"].value").expect("valid entry point");
let entrypoints = [entrypoint.as_ptr()];
let compiled =
regorus_engine_compile_program_with_entrypoints(engine, entrypoints.as_ptr(), 1);
assert!(
matches!(compiled.status, RegorusStatus::Ok),
"RVM compilation failed with {:?}",
compiled.status
);
let program = compiled.pointer_value as *mut RegorusProgram;
assert!(!program.is_null());
regorus_result_drop(compiled);

let vm = regorus_rvm_new();
assert!(!vm.is_null());
let loaded = regorus_rvm_load_program(vm, program);
assert!(matches!(loaded.status, RegorusStatus::Ok));
regorus_result_drop(loaded);

let named = regorus_rvm_execute_entry_point_by_name(vm, entrypoint.as_ptr());
assert!(matches!(named.status, RegorusStatus::Ok));
assert_eq!(
unsafe { CStr::from_ptr(named.output) }
.to_str()
.expect("UTF-8 result"),
"7"
);
regorus_result_drop(named);

let indexed = regorus_rvm_execute_entry_point_by_index(vm, 0);
assert!(matches!(indexed.status, RegorusStatus::Ok));
assert_eq!(
unsafe { CStr::from_ptr(indexed.output) }
.to_str()
.expect("UTF-8 result"),
"7"
);
regorus_result_drop(indexed);

regorus_rvm_drop(vm);
regorus_program_drop(program);
regorus_engine_drop(engine);
}
}
18 changes: 18 additions & 0 deletions docs/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ referenced by:
path-ref ::= IDENT ( NO_WS ( '.' NO_WS IDENT | '[' STRING ']' ) )*
```

String components in brackets retain their literal identity, including dots.
For example, `graph["a.b"]` is distinct from `graph.a.b`. When a package path
is returned by the Engine API or used as a rule entry point, non-identifier
string components use JSON-escaped bracket notation.

Policy-source path references are limited to 32 components, counting the
initial identifier as one. Every dotted identifier or bracketed string adds
one component; dots inside a bracketed string do not. A 33rd component is
rejected by the general policy parser.

Static public entry-point lookup uses a separate iterative parser rather than
building the recursively nested policy AST. It accepts dotted and bracketed
string spellings up to the longest registered rule path, so equivalent
spellings remain consistent even when that path exceeds the policy parser's
32-component limit. Paths that exceed the registered depth or do not identify
a rule remain invalid; `Engine::eval_rule` reports `not a valid rule path`,
while parser and compiler APIs return an error.

referenced by:

* call-expr
Expand Down
3 changes: 3 additions & 0 deletions src/compiled_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ pub(crate) struct CompiledPolicyData {
pub(crate) schedule: Option<Rc<Schedule>>,
pub(crate) rules: Map<String, Vec<Ref<Rule>>>,
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
pub(crate) rule_path_components: Map<String, Vec<PathComponent>>,
// Canonical string paths can coalesce rules with distinct typed selectors.
pub(crate) rule_path_components_by_rule: BTreeMap<Ref<Rule>, Vec<PathComponent>>,
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
pub(crate) functions: FunctionTable,
pub(crate) rule_paths: MapSet<String>,
Expand Down
Loading
Loading