diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle
index f0b81b4b..62d3f743 100644
--- a/CedarJava/build.gradle
+++ b/CedarJava/build.gradle
@@ -67,6 +67,12 @@ spotbugs {
ignoreFailures.set(false)
}
+tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach {
+ if (name == 'spotbugsJmh') {
+ excludeFilter = file('config/spotbugs/jmh-exclude.xml')
+ }
+}
+
repositories {
mavenCentral()
}
@@ -75,6 +81,15 @@ configurations {
testCompileOnly.extendsFrom compileOnly
}
+sourceSets {
+ jmh {
+ java.srcDirs = ['src/jmh/java']
+ resources.srcDirs = ['src/jmh/resources', 'src/test/resources']
+ compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath
+ runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath
+ }
+}
+
dependencies {
def junitVersion = '6.0.0'
@@ -93,6 +108,9 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junitVersion}"
+
+ jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
+ jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}
def ffiDir = '../CedarJavaFFI'
@@ -127,7 +145,7 @@ tasks.register('installCargoZigbuild', Exec) {
group 'Build'
description 'Installs Cargo Zigbuild for Rust compilation.'
- commandLine 'cargo', '+' + RustVersion, 'install', 'cargo-zigbuild@0.22.1'
+ commandLine 'cargo', '+' + RustVersion, 'install', '--locked', 'cargo-zigbuild@0.22.1'
}
def ZigVersion = '0.11'
@@ -261,6 +279,27 @@ compileTestJava {
targetCompatibility = "17"
}
+compileJmhJava {
+ sourceCompatibility = "17"
+ targetCompatibility = "17"
+}
+
+tasks.register('jmh', JavaExec) {
+ group 'Benchmark'
+ description 'Runs JMH benchmarks for JNI performance.'
+
+ dependsOn('compileFFI')
+ dependsOn('jmhClasses')
+
+ mainClass = 'org.openjdk.jmh.Main'
+ classpath = sourceSets.jmh.runtimeClasspath + files(layout.buildDirectory.dir(compiledLibDir))
+
+ // Pass through any -Pjmh.args="..." to JMH (e.g., -Pjmh.args="-f 0 -i 1 -wi 1")
+ if (project.hasProperty('jmh.args')) {
+ args((project.property('jmh.args') as String).split('\\s+'))
+ }
+}
+
compileJava {
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
diff --git a/CedarJava/config/spotbugs/jmh-exclude.xml b/CedarJava/config/spotbugs/jmh-exclude.xml
new file mode 100644
index 00000000..1d435f26
--- /dev/null
+++ b/CedarJava/config/spotbugs/jmh-exclude.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/CedarJava/src/jmh/java/com/cedarpolicy/AuthorizationBenchmark.java b/CedarJava/src/jmh/java/com/cedarpolicy/AuthorizationBenchmark.java
new file mode 100644
index 00000000..e344176a
--- /dev/null
+++ b/CedarJava/src/jmh/java/com/cedarpolicy/AuthorizationBenchmark.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright Cedar Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.cedarpolicy;
+
+import com.cedarpolicy.model.AuthorizationRequest;
+import com.cedarpolicy.model.AuthorizationResponse;
+import com.cedarpolicy.model.ValidationRequest;
+import com.cedarpolicy.model.ValidationResponse;
+import com.cedarpolicy.model.entity.Entities;
+import com.cedarpolicy.model.entity.Entity;
+import com.cedarpolicy.model.exception.AuthException;
+import com.cedarpolicy.model.policy.PolicySet;
+import com.cedarpolicy.model.schema.Schema;
+import com.cedarpolicy.model.schema.Schema.JsonOrCedar;
+import com.cedarpolicy.value.EntityTypeName;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JMH benchmarks for Cedar authorization engine JNI performance.
+ *
+ *
Run via: ./gradlew jmh
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@State(Scope.Benchmark)
+public class AuthorizationBenchmark {
+
+ private BasicAuthorizationEngine engine;
+
+ // Small scenario: minimal request
+ private AuthorizationRequest smallRequest;
+ private PolicySet smallPolicySet;
+ private Set smallEntities;
+
+ // Medium scenario: photoflash schema with entities and multiple policies
+ private AuthorizationRequest mediumRequest;
+ private PolicySet mediumPolicySet;
+ private Set mediumEntities;
+
+ // Validation scenario
+ private ValidationRequest validationRequest;
+
+ @Setup(Level.Trial)
+ public void setUp() throws Exception {
+ engine = new BasicAuthorizationEngine();
+
+ setUpSmallScenario();
+ setUpMediumScenario();
+ setUpValidationScenario();
+ }
+
+ private static String readResource(String resourcePath) throws Exception {
+ return new String(
+ Files.readAllBytes(Paths.get(
+ AuthorizationBenchmark.class.getResource(resourcePath).toURI())),
+ StandardCharsets.UTF_8);
+ }
+
+ private void setUpSmallScenario() throws Exception {
+ EntityTypeName userType = EntityTypeName.parse("User").get();
+ EntityTypeName actionType = EntityTypeName.parse("Action").get();
+ EntityTypeName resourceType = EntityTypeName.parse("Resource").get();
+
+ smallRequest = new AuthorizationRequest(
+ userType.of("alice"), actionType.of("view"), resourceType.of("doc1"), new HashMap<>());
+
+ smallPolicySet = PolicySet.parsePolicies(readResource("/small_policies.cedar"));
+ smallEntities = Entities.parse(readResource("/small_entities.json")).getEntities();
+ }
+
+ private void setUpMediumScenario() throws Exception {
+ EntityTypeName userType = EntityTypeName.parse("User").get();
+ EntityTypeName actionType = EntityTypeName.parse("Action").get();
+ EntityTypeName photoType = EntityTypeName.parse("Photo").get();
+
+ mediumRequest = new AuthorizationRequest(
+ userType.of("alice"), actionType.of("View_Photo"), photoType.of("pic01"), new HashMap<>());
+
+ mediumPolicySet = PolicySet.parsePolicies(readResource("/medium_policies.cedar"));
+ mediumEntities = Entities.parse(readResource("/medium_entities.json")).getEntities();
+ }
+
+ private void setUpValidationScenario() throws Exception {
+ String schemaText = readResource("/photoflash_schema.json");
+ Schema schema = new Schema(JsonOrCedar.Json, Optional.of(schemaText), Optional.empty());
+
+ PolicySet policySet = PolicySet.parsePolicies(
+ "permit(principal == User::\"alice\", action == Action::\"View_Photo\", resource);");
+
+ validationRequest = new ValidationRequest(schema, policySet);
+ }
+
+ @Benchmark
+ public AuthorizationResponse isAuthorizedSmall() throws AuthException {
+ return engine.isAuthorized(smallRequest, smallPolicySet, smallEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse isAuthorizedMedium() throws AuthException {
+ return engine.isAuthorized(mediumRequest, mediumPolicySet, mediumEntities);
+ }
+
+ @Benchmark
+ public ValidationResponse validateSmall() throws AuthException {
+ return engine.validate(validationRequest);
+ }
+}
diff --git a/CedarJava/src/jmh/resources/medium_entities.json b/CedarJava/src/jmh/resources/medium_entities.json
new file mode 100644
index 00000000..99c3194b
--- /dev/null
+++ b/CedarJava/src/jmh/resources/medium_entities.json
@@ -0,0 +1,7 @@
+[
+ {"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "View_Photo"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Photo", "id": "pic01"}, "attrs": {}, "parents": [{"type": "Album", "id": "vacation"}]},
+ {"uid": {"type": "Album", "id": "vacation"}, "attrs": {}, "parents": [{"type": "Account", "id": "account1"}]},
+ {"uid": {"type": "Account", "id": "account1"}, "attrs": {}, "parents": []}
+]
diff --git a/CedarJava/src/jmh/resources/medium_policies.cedar b/CedarJava/src/jmh/resources/medium_policies.cedar
new file mode 100644
index 00000000..1b00713d
--- /dev/null
+++ b/CedarJava/src/jmh/resources/medium_policies.cedar
@@ -0,0 +1,9 @@
+permit(principal == User::"alice", action == Action::"View_Photo", resource);
+
+permit(principal, action == Action::"View_Photo", resource in Album::"vacation");
+
+forbid(principal, action, resource) when { resource.private };
+
+permit(principal, action == Action::"Edit_Photo", resource) when { principal == resource.owner };
+
+permit(principal, action == Action::"Delete_Photo", resource) when { principal == resource.owner };
diff --git a/CedarJava/src/jmh/resources/small_entities.json b/CedarJava/src/jmh/resources/small_entities.json
new file mode 100644
index 00000000..49905922
--- /dev/null
+++ b/CedarJava/src/jmh/resources/small_entities.json
@@ -0,0 +1,5 @@
+[
+ {"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "view"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Resource", "id": "doc1"}, "attrs": {}, "parents": []}
+]
diff --git a/CedarJava/src/jmh/resources/small_policies.cedar b/CedarJava/src/jmh/resources/small_policies.cedar
new file mode 100644
index 00000000..41c318cd
--- /dev/null
+++ b/CedarJava/src/jmh/resources/small_policies.cedar
@@ -0,0 +1 @@
+permit(principal, action, resource);
diff --git a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java
index e794cdbe..26f65842 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java
@@ -42,7 +42,6 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -50,6 +49,13 @@
public final class BasicAuthorizationEngine implements AuthorizationEngine {
static {
LibraryLoader.loadLibrary();
+ String jniVersion = getCedarJNIVersion();
+ String langVersion = AuthorizationEngine.getCedarLangVersion();
+ if (!jniVersion.equals(langVersion)) {
+ throw new ExceptionInInitializerError(
+ "Error, Java Cedar Language version is " + langVersion
+ + " but JNI Cedar Language version is " + jniVersion);
+ }
}
/** Construct a basic authorization engine. */
@@ -123,21 +129,11 @@ public void validateEntities(EntityValidationRequest q) throws AuthException {
private static RESP call(String operation, Class responseClass, REQ request)
throws AuthException {
try {
- final String cedarJNIVersion = getCedarJNIVersion();
- if (!cedarJNIVersion.equals(AuthorizationEngine.getCedarLangVersion())) {
- throw new AuthException(
- "Error, Java Cedar Language version is "
- + AuthorizationEngine.getCedarLangVersion()
- + " but JNI Cedar Language version is "
- + cedarJNIVersion);
- }
- // Convert the request POJO to a JSON string
final String fullRequest = objectWriter().writeValueAsString(request);
final String response = callCedarJNI(operation, fullRequest);
- final JsonNode responseNode = objectReader().readTree(response);
- return objectReader().readValue(responseNode, responseClass);
+ return objectReader().readValue(response, responseClass);
} catch (JsonProcessingException e) {
throw new AuthException("JSON Serialization Error", e);
} catch (IllegalArgumentException e) {
diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java
index ee1792a9..68423cc5 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java
@@ -127,7 +127,7 @@ public DetailedError getError() {
@Override
public String toString() {
- return String.format("AuthorizationError{policyId=%s, error=%s}", policyId, error);
+ return String.format("AuthorizationError{policyId=%s, error=%s}", policyId, error);
}
}
diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java
index 166149b8..d24141cb 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java
@@ -28,7 +28,7 @@
public final class LevelValidationRequest {
private final Schema schema;
private final PolicySet policies;
- private final long maxDerefLevel; // Must be non-negative (>=0)
+ private final long maxDerefLevel; // Must be non-negative (>=0)
/**
* Construct a validation request.
@@ -77,7 +77,7 @@ public PolicySet getPolicySet() {
/**
* Get the maximum deref level.
- *
+ *
* @return The maximum deref level value for validation
*/
public long getMaxDerefLevel() {
diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
index 7a35e951..d23b6545 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
@@ -148,8 +148,8 @@ public String toCedarFormat() throws InternalException, IllegalStateException, N
* @return JsonNode representing the schema in JSON format
* @throws InternalException If conversion from Cedar to JSON format fails
* @throws IllegalStateException If schema content is missing
- * @throws JsonMappingException If invalid JSON
- * @throws JsonProcessingException If invalid JSON
+ * @throws JsonMappingException If invalid JSON
+ * @throws JsonProcessingException If invalid JSON
* @throws NullPointerException
*/
public JsonNode toJsonFormat()
diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java
index d4df41d6..943ea951 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java
@@ -59,12 +59,6 @@ public class ValueDeserializer extends JsonDeserializer {
private static final Set MULTI_ARG_FN = Set.of(FN_OFFSET);
private static final Set SINGLE_ARG_FN = Set.of(FN_IP, FN_DECIMAL, FN_UNKNOWN, FN_DATETIME, FN_DURATION);
- private enum EscapeType {
- ENTITY,
- EXTENSION,
- UNRECOGNIZED
- }
-
/** Deserialize Json to Value. */
@Override
public Value deserialize(JsonParser parser, DeserializationContext context) throws IOException {
@@ -85,68 +79,47 @@ public Value deserialize(JsonParser parser, DeserializationContext context) thro
}
return myNode;
} else if (node.isObject()) {
- Iterator> iter = node.fields();
- // Do two passes, one to check if it is an escaped entity or extension and a second to
- // write into a map
- EscapeType escapeType = EscapeType.UNRECOGNIZED;
- int count = 0;
- while (iter.hasNext()) {
- count++;
- Map.Entry entry = iter.next();
- if (entry.getKey().equals(ENTITY_ESCAPE_SEQ)) {
- escapeType = EscapeType.ENTITY;
- } else if (entry.getKey().equals(EXTENSION_ESCAPE_SEQ)) {
- escapeType = EscapeType.EXTENSION;
- }
- }
- if (escapeType != EscapeType.UNRECOGNIZED) {
- if (count == 1) {
- if (escapeType == EscapeType.ENTITY) {
- JsonNode val = node.get(ENTITY_ESCAPE_SEQ);
- if (val.isObject() && val.has("id") && val.has("type")) {
- int numFields = 0;
- for (Iterator it = val.fieldNames(); it.hasNext(); it.next()) {
- numFields++;
- }
- if (numFields == 2) {
- EntityIdentifier id = new EntityIdentifier(val.get("id").textValue());
- Optional type = EntityTypeName.parse(val.get("type").textValue());
- if (type.isPresent()) {
- return new EntityUID(type.get(), id);
- } else {
- String msg = "Invalid Entity Type" + val.get("type").textValue();
- throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Map.class);
- }
- }
+ // Single-pass: check for escape sequences via direct lookup, avoiding full iteration
+ if (node.size() == 1) {
+ if (node.has(ENTITY_ESCAPE_SEQ)) {
+ JsonNode val = node.get(ENTITY_ESCAPE_SEQ);
+ if (val.has("id") && val.has("type") && val.size() == 2) {
+ EntityIdentifier id = new EntityIdentifier(val.get("id").textValue());
+ Optional type = EntityTypeName.parse(val.get("type").textValue());
+ if (type.isPresent()) {
+ return new EntityUID(type.get(), id);
+ } else {
+ String msg = "Invalid Entity Type" + val.get("type").textValue();
+ throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Map.class);
}
+ }
+ throw new InvalidValueDeserializationException(parser,
+ "Not textual node: " + node.toString(), node.asToken(), Map.class);
+ } else if (node.has(EXTENSION_ESCAPE_SEQ)) {
+ JsonNode val = node.get(EXTENSION_ESCAPE_SEQ);
+ JsonNode fn = val.get("fn");
+ if (!fn.isTextual()) {
throw new InvalidValueDeserializationException(parser,
- "Not textual node: " + node.toString(), node.asToken(), Map.class);
- } else {
- JsonNode val = node.get(EXTENSION_ESCAPE_SEQ);
- JsonNode fn = val.get("fn");
- if (!fn.isTextual()) {
- throw new InvalidValueDeserializationException(parser,
- "Not textual node: " + fn.toString(), node.asToken(), Map.class);
- }
+ "Not textual node: " + fn.toString(), node.asToken(), Map.class);
+ }
- String fnName = fn.textValue();
+ String fnName = fn.textValue();
- if (MULTI_ARG_FN.contains(fnName)) {
- return deserializeMultiArgFunction(fnName, val, mapper, parser, node);
- } else if (SINGLE_ARG_FN.contains(fnName)) {
- return deserializeSingleArgFunction(fnName, val, parser, node);
- } else {
- throw new InvalidValueDeserializationException(parser,
- "Invalid function type: " + fn.toString(), node.asToken(), Map.class);
- }
+ if (MULTI_ARG_FN.contains(fnName)) {
+ return deserializeMultiArgFunction(fnName, val, mapper, parser, node);
+ } else if (SINGLE_ARG_FN.contains(fnName)) {
+ return deserializeSingleArgFunction(fnName, val, parser, node);
+ } else {
+ throw new InvalidValueDeserializationException(parser,
+ "Invalid function type: " + fn.toString(), node.asToken(), Map.class);
}
- } else {
- throw new InvalidValueDeserializationException(parser,
- "More than one K,V pair with {__entity, __extn}: " + node.toString(), node.asToken(), Map.class);
}
+ } else if (node.size() > 1 && (node.has(ENTITY_ESCAPE_SEQ) || node.has(EXTENSION_ESCAPE_SEQ))) {
+ throw new InvalidValueDeserializationException(parser,
+ "More than one K,V pair with {__entity, __extn}: " + node.toString(), node.asToken(), Map.class);
}
CedarMap myMap = new CedarMap();
- iter = node.fields();
+ Iterator> iter = node.fields();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
myMap.put(entry.getKey(), mapper.treeToValue(entry.getValue(), Value.class));
diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/Decimal.java b/CedarJava/src/main/java/com/cedarpolicy/value/Decimal.java
index 320027f7..0ed4b1e7 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/value/Decimal.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/value/Decimal.java
@@ -17,10 +17,9 @@
package com.cedarpolicy.value;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.math.BigDecimal;
import java.util.Objects;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
/**
* Represents a Cedar fixed-point decimal extension value. Decimals are encoded as strings in
@@ -29,22 +28,21 @@
public class Decimal extends Value {
private static class DecimalValidator {
- private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?([0-9])*(\\.)([0-9]{0,4})$");
+ private static final Pattern DECIMAL_PATTERN =
+ Pattern.compile("^-?[0-9]+\\.[0-9]{1,4}$");
+ private static final BigDecimal RANGE_MIN = new BigDecimal("-922337203685477.5808");
+ private static final BigDecimal RANGE_MAX = new BigDecimal("922337203685477.5807");
public static boolean validDecimal(String d) {
if (d == null || d.isEmpty()) {
return false;
}
- d = d.trim();
- if (d.length() > 21) {
- return false; // 19digits, decimal point and - sign
- }
- try {
- Matcher matcher = DECIMAL_PATTERN.matcher(d);
- return matcher.matches();
- } catch (PatternSyntaxException ex) {
+ if (!DECIMAL_PATTERN.matcher(d).matches()) {
return false;
}
+
+ BigDecimal val = new BigDecimal(d);
+ return val.compareTo(RANGE_MIN) >= 0 && val.compareTo(RANGE_MAX) <= 0;
}
}
diff --git a/CedarJava/src/test/java/com/cedarpolicy/DecimalTests.java b/CedarJava/src/test/java/com/cedarpolicy/DecimalTests.java
new file mode 100644
index 00000000..c2446bad
--- /dev/null
+++ b/CedarJava/src/test/java/com/cedarpolicy/DecimalTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright Cedar Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.cedarpolicy;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import com.cedarpolicy.value.Decimal;
+
+public class DecimalTests {
+
+ @Test
+ public void testValidDecimals() {
+ assertDoesNotThrow(() -> new Decimal("1.0"));
+ assertDoesNotThrow(() -> new Decimal("1.0000"));
+ assertDoesNotThrow(() -> new Decimal("-1.0"));
+ assertDoesNotThrow(() -> new Decimal("0.0"));
+ assertDoesNotThrow(() -> new Decimal("0.0000"));
+ assertDoesNotThrow(() -> new Decimal("123.456"));
+ assertDoesNotThrow(() -> new Decimal("-123.456"));
+ // Negative zero is valid
+ assertDoesNotThrow(() -> new Decimal("-0.0"));
+ assertDoesNotThrow(() -> new Decimal("-0.0000"));
+ }
+
+ @Test
+ public void testLeadingZeros() {
+ // Leading zeros should be accepted - Cedar trims them
+ assertDoesNotThrow(() -> new Decimal("001.0"));
+ assertDoesNotThrow(() -> new Decimal("0001.0000"));
+ assertDoesNotThrow(() -> new Decimal("-001.0"));
+ assertDoesNotThrow(() -> new Decimal("00000000000000000001.0"));
+ }
+
+ @Test
+ public void testBoundaryValues() {
+ // Max positive: 922337203685477.5807
+ assertDoesNotThrow(() -> new Decimal("922337203685477.5807"));
+ // Min negative: -922337203685477.5808
+ assertDoesNotThrow(() -> new Decimal("-922337203685477.5808"));
+ // With leading zeros
+ assertDoesNotThrow(() -> new Decimal("-000000922337203685477.5808"));
+ assertDoesNotThrow(() -> new Decimal("000000922337203685477.5807"));
+ }
+
+ @Test
+ public void testOutOfRange() {
+ // One above max positive
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("922337203685477.5808"));
+ // One below min negative
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("-922337203685477.5809"));
+ // Way out of range
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("9999999999999999.0"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("-9999999999999999.0"));
+ // Out of range even with leading zeros
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("000922337203685477.5808"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("-000922337203685477.5809"));
+ }
+
+ @Test
+ public void testInvalidFormat() {
+ assertThrows(IllegalArgumentException.class, () -> new Decimal(""));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("abc"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1.00000"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal(null));
+ }
+
+ @Test
+ public void testEdgeCaseFormats() {
+ // Missing integer part before dot
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("."));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal(".1"));
+ // Missing fractional part after dot
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1."));
+ // Negative zero with no fractional digits
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("-.0"));
+ // Multiple dots
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1.2.3"));
+ // Consecutive dots
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("-.."));
+ // Explicit plus sign
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("+1.0"));
+ // Double negative
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("--1.0"));
+ // Scientific notation
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1.0e2"));
+ // Whitespace in the middle
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1 .0"));
+ // Leading/trailing whitespace
+ assertThrows(IllegalArgumentException.class, () -> new Decimal(" 1.0"));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal("1.0 "));
+ assertThrows(IllegalArgumentException.class, () -> new Decimal(" 1.0 "));
+ }
+
+ @Test
+ public void testToCedarExpr() {
+ Decimal d = new Decimal("1.0000");
+ assertEquals("decimal(\"1.0000\")", d.toCedarExpr());
+ }
+
+ @Test
+ public void testEquality() {
+ Decimal d1 = new Decimal("1.0000");
+ Decimal d2 = new Decimal("1.0000");
+ assertEquals(d1, d2);
+ assertEquals(d1.hashCode(), d2.hashCode());
+ }
+}
diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs
index df70898c..e25676fc 100644
--- a/CedarJavaFFI/src/interface.rs
+++ b/CedarJavaFFI/src/interface.rs
@@ -33,7 +33,7 @@ use jni::{
use jni_fn::jni_fn;
use serde::{Deserialize, Serialize};
use serde_json::{from_str, Value};
-use std::{error::Error, str::FromStr, thread};
+use std::{error::Error, panic, str::FromStr};
use crate::{
answer::Answer,
@@ -65,10 +65,6 @@ fn build_err_obj(env: &JNIEnv<'_>, err: &str) -> jstring {
.into_raw()
}
-fn call_cedar_in_thread(call_str: String, input_str: String) -> String {
- call_cedar(&call_str, &input_str)
-}
-
/// JNI entry point for authorization and validation requests
#[jni_fn("com.cedarpolicy.BasicAuthorizationEngine")]
pub fn callCedarJNI(
@@ -82,17 +78,14 @@ pub fn callCedarJNI(
_ => return build_err_obj(&env, "getting"),
};
- let mut j_input_str: String = match env.get_string(&j_input) {
+ let j_input_str: String = match env.get_string(&j_input) {
Ok(s) => s.into(),
Err(_) => return build_err_obj(&env, "parsing"),
};
- j_input_str.push(' ');
-
- let handle = thread::spawn(move || call_cedar_in_thread(j_call_str, j_input_str));
- let result = match handle.join() {
+ let result = match panic::catch_unwind(|| call_cedar(&j_call_str, &j_input_str)) {
Ok(s) => s,
- Err(e) => format!("Authorization thread failed {e:?}"),
+ Err(e) => format!("Authorization call panicked: {e:?}"),
};
let res = env.new_string(result);