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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class BuiltinRegistry {
AggregateBuiltins.class,
ArithmeticBuiltins.class,
ArrayBuiltins.class,
BitsBuiltins.class,
EncodingBuiltins.class,
HexBuiltins.class,
StringBuiltins.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.github.open_policy_agent.opa.ast.builtin.impls;

import static io.github.open_policy_agent.opa.ast.builtin.impls.utils.ArgHelper.getArg;

import java.util.Map;
import java.util.function.BiFunction;

import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin;
import io.github.open_policy_agent.opa.ast.builtin.OpaDynamic;
import io.github.open_policy_agent.opa.ast.builtin.OpaType;
import io.github.open_policy_agent.opa.ast.types.RegoBigInt;
import io.github.open_policy_agent.opa.ast.types.RegoNumber;
import io.github.open_policy_agent.opa.ast.types.RegoValue;
import io.github.open_policy_agent.opa.rego.EvaluationContext;
import io.github.open_policy_agent.opa.rego.TypeError;

public class BitsBuiltins {

public static Map<String, BiFunction<EvaluationContext, RegoValue[], RegoValue>> builtins() {
BitsBuiltins instance = new BitsBuiltins();
return Map.ofEntries(
Map.entry("bits.and", instance::and),
Map.entry("bits.or", instance::or),
Map.entry("bits.xor", instance::xor),
Map.entry("bits.negate", instance::negate),
Map.entry("bits.lsh", instance::lsh),
Map.entry("bits.rsh", instance::rsh));
}

@OpaBuiltin(
name = "bits.and",
description = "Returns the bitwise AND of two integers.",
args = {
@OpaType(type = "number", name = "x", description = "the first integer", dynamic = @OpaDynamic(type = "any")),
@OpaType(type = "number", name = "y", description = "the second integer", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the bitwise AND of x and y", dynamic = @OpaDynamic(type = "any")))
public RegoValue and(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
RegoNumber y = getArg(args, 1, RegoNumber.class);
requireInteger(x, "bits.and", 1);
requireInteger(y, "bits.and", 2);
return new RegoBigInt(x.getBigIntValue().and(y.getBigIntValue()));
}

@OpaBuiltin(
name = "bits.or",
description = "Returns the bitwise OR of two integers.",
args = {
@OpaType(type = "number", name = "x", description = "the first integer", dynamic = @OpaDynamic(type = "any")),
@OpaType(type = "number", name = "y", description = "the second integer", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the bitwise OR of x and y", dynamic = @OpaDynamic(type = "any")))
public RegoValue or(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
RegoNumber y = getArg(args, 1, RegoNumber.class);
requireInteger(x, "bits.or", 1);
requireInteger(y, "bits.or", 2);
return new RegoBigInt(x.getBigIntValue().or(y.getBigIntValue()));
}

@OpaBuiltin(
name = "bits.xor",
description = "Returns the bitwise XOR of two integers.",
args = {
@OpaType(type = "number", name = "x", description = "the first integer", dynamic = @OpaDynamic(type = "any")),
@OpaType(type = "number", name = "y", description = "the second integer", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the bitwise XOR of x and y", dynamic = @OpaDynamic(type = "any")))
public RegoValue xor(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
RegoNumber y = getArg(args, 1, RegoNumber.class);
requireInteger(x, "bits.xor", 1);
requireInteger(y, "bits.xor", 2);
return new RegoBigInt(x.getBigIntValue().xor(y.getBigIntValue()));
}

@OpaBuiltin(
name = "bits.negate",
description = "Returns the bitwise negation of an integer.",
args = {
@OpaType(type = "number", name = "x", description = "the integer to negate", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the bitwise negation of x", dynamic = @OpaDynamic(type = "any")))
public RegoValue negate(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
requireInteger(x, "bits.negate", 1);
return new RegoBigInt(x.getBigIntValue().not());
}

@OpaBuiltin(
name = "bits.lsh",
description = "Returns the result of shifting x left by s bits.",
args = {
@OpaType(type = "number", name = "x", description = "the integer to shift", dynamic = @OpaDynamic(type = "any")),
@OpaType(type = "number", name = "s", description = "the number of bits to shift", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the result of shifting x s bits to the left", dynamic = @OpaDynamic(type = "any")))
public RegoValue lsh(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
RegoNumber s = getArg(args, 1, RegoNumber.class);
requireInteger(x, "bits.lsh", 1);
requireUnsigned(s, "bits.lsh");
return new RegoBigInt(x.getBigIntValue().shiftLeft(s.getBigIntValue().intValueExact()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shift operand s is never validated as an integer, so fractional shifts are silently truncated instead of erroring. lsh/rsh call requireInteger on x but only requireUnsigned on s — never requireInteger(s, ...). For bits.lsh(1, 2.5), RegoDecimal.getBigIntValue() does BigInteger.valueOf((long) 2.5) = 2, so this returns 4. OPA raises bits.lsh: operand 2 must be integer number but got floating-point number (undefined without --strict-builtin-errors, eval_type_error with it) — confirmed on the OPA binary. Reachable via JSON input {"s": 2.5}. Same issue in rsh at line 120.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intValueExact() throws an uncaught ArithmeticException for shift amounts above Integer.MAX_VALUE. s.getBigIntValue().intValueExact() throws ArithmeticException (not a TypeError/BuiltinError) for e.g. bits.lsh(1, 2147483648), so it escapes the builtin error path as an unexpected exception. OPA instead does uint(b.Uint64()) and attempts the shift. Lower severity (huge shifts are impractical either way), but the failure mode differs from a clean Rego error. Also applies to rsh at line 120.

}

@OpaBuiltin(
name = "bits.rsh",
description = "Returns the result of shifting x right by s bits.",
args = {
@OpaType(type = "number", name = "x", description = "the integer to shift", dynamic = @OpaDynamic(type = "any")),
@OpaType(type = "number", name = "s", description = "the number of bits to shift", dynamic = @OpaDynamic(type = "any"))
},
result = @OpaType(type = "number", name = "z", description = "the result of shifting x s bits to the right", dynamic = @OpaDynamic(type = "any")))
public RegoValue rsh(EvaluationContext ctx, RegoValue[] args) {
RegoNumber x = getArg(args, 0, RegoNumber.class);
RegoNumber s = getArg(args, 1, RegoNumber.class);
requireInteger(x, "bits.rsh", 1);
requireUnsigned(s, "bits.rsh");
return new RegoBigInt(x.getBigIntValue().shiftRight(s.getBigIntValue().intValueExact()));
}

private static void requireInteger(RegoNumber n, String fn, int operandIndex) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer-valued floats are wrongly rejected across all six functions. requireInteger throws for any isDecimal(), but OPA accepts floats that are exactly integral (its NumberToInt only fails when the value isn't big.Exact). Confirmed on the OPA binary: bits.and(2.0, 3)2, bits.negate(5.0)-6, bits.lsh(1, 2.0)4, but all throw TypeError here. Reachable via JSON input {"n": 2.0}, which Jackson maps to RegoDecimal (RegoValueModule.java:182-183). Static Rego literals like 2.0 are safe (TypeUtils.parseStringToNumber folds whole floats to integer types), so only input-supplied values trigger it. Suggest checking integrality (Double.isFinite(d) && d == Math.rint(d)) rather than isDecimal().

if (n.isDecimal()) {
throw new TypeError(fn + ": operand " + operandIndex + " must be integer number but got floating-point number");
}
}

private static void requireUnsigned(RegoNumber s, String fn) {
if (s.getBigIntValue().signum() < 0) {
throw new TypeError(fn + ": operand 2 must be an unsigned integer number but got a negative integer");
}
}
}