roryqi commented on code in PR #13478:
URL: https://github.com/apache/gravitino/pull/13478#discussion_r4246033891


##########
core/src/main/java/org/apache/gravitino/policy/rego/CanonicalExpression.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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 org.apache.gravitino.policy.rego;
+
+import com.google.common.base.Preconditions;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A node in an unresolved {@code restricted-rego-v1} expression tree.
+ *
+ * <p>The tree contains only the source constructs allowlisted by the 
read-restriction contract.
+ * Column and context references remain symbolic until the effective-policy 
resolver binds them to a
+ * table schema and request context.
+ */
+public interface CanonicalExpression {
+
+  /**
+   * Validates this expression node and all of its children.
+   *
+   * @throws IllegalArgumentException if the expression is malformed or 
outside the allowlisted
+   *     source profile
+   */
+  void validate() throws IllegalArgumentException;
+
+  /**
+   * Returns the operation depth defined by {@code restricted-rego-v1}.
+   *
+   * <p>Value leaves such as a column, string, or number have depth 0. Atomic 
predicates such as a
+   * comparison, Boolean literal, or group-membership check have depth 1. 
Negation and a logical
+   * expression add one to their deepest operand. A consecutive n-ary logical 
chain counts as one
+   * operation, so {@code true and false and true} has depth 2.
+   *
+   * @return expression operation depth
+   */
+  int depth();
+
+  /** Comparison operators supported by {@code restricted-rego-v1}. */
+  enum ComparisonOperator {
+    /** Equality comparison. */
+    EQ("=="),
+    /** Inequality comparison. */
+    NEQ("!="),
+    /** Less-than comparison. */
+    LT("<"),
+    /** Less-than-or-equal comparison. */
+    LTE("<="),
+    /** Greater-than comparison. */
+    GT(">"),
+    /** Greater-than-or-equal comparison. */
+    GTE(">="),
+    /** Literal-array membership comparison. */
+    IN("in");
+
+    private final String sourceToken;
+
+    ComparisonOperator(String sourceToken) {
+      this.sourceToken = sourceToken;
+    }
+
+    /**
+     * Parses an operator token from restricted Rego source.
+     *
+     * @param token source operator token
+     * @return parsed operator
+     */
+    public static ComparisonOperator fromSourceToken(String token) {
+      Preconditions.checkArgument(token != null && !token.isEmpty(), "operator 
cannot be empty");
+      switch (token) {
+        case "==":
+          return EQ;
+        case "!=":
+          return NEQ;
+        case "<":
+          return LT;
+        case "<=":
+          return LTE;
+        case ">":
+          return GT;
+        case ">=":
+          return GTE;
+        case "in":
+          return IN;
+        default:
+          throw new IllegalArgumentException("Unsupported restricted-rego-v1 
operator: " + token);
+      }
+    }
+
+    /**
+     * Returns the token used in restricted Rego source.
+     *
+     * @return source token
+     */
+    public String sourceToken() {
+      return sourceToken;
+    }
+  }
+
+  /** N-ary logical operators supported by {@code restricted-rego-v1}. */
+  enum LogicalOperator {
+    /** Boolean conjunction. */
+    AND("and"),
+    /** Boolean disjunction. */
+    OR("or");
+
+    private final String sourceToken;
+
+    LogicalOperator(String sourceToken) {
+      this.sourceToken = sourceToken;
+    }
+
+    /**
+     * Returns the token used in restricted Rego source.
+     *
+     * @return source token
+     */
+    public String sourceToken() {
+      return sourceToken;
+    }
+  }
+
+  /** Source literal types supported by {@code restricted-rego-v1}. */
+  enum LiteralType {
+    /** String literal. */
+    STRING,
+    /** Exact base-10 numeric literal. */
+    NUMBER,
+    /** Boolean literal. */
+    BOOLEAN,
+    /** Null literal. */
+    NULL
+  }
+
+  /** A binary comparison operation. */
+  final class Comparison implements CanonicalExpression {
+    private final ComparisonOperator operator;
+    private final CanonicalExpression left;
+    private final CanonicalExpression right;
+
+    Comparison(ComparisonOperator operator, CanonicalExpression left, 
CanonicalExpression right) {
+      this.operator = operator;
+      this.left = left;
+      this.right = right;
+    }
+
+    /**
+     * Returns the comparison operator.
+     *
+     * @return comparison operator
+     */
+    public ComparisonOperator operator() {
+      return operator;
+    }
+
+    /**
+     * Returns the left operand.
+     *
+     * @return left operand
+     */
+    public CanonicalExpression left() {
+      return left;
+    }
+
+    /**
+     * Returns the right operand.
+     *
+     * @return right operand
+     */
+    public CanonicalExpression right() {
+      return right;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(operator != null, "comparison operator 
cannot be null");
+      Preconditions.checkArgument(
+          left != null && right != null, "comparison operands cannot be null");
+      left.validate();
+      right.validate();
+      try {
+        ExpressionValidator.validateComparison(operator, left, right);
+      } catch (IllegalArgumentException exception) {
+        throw new IllegalArgumentException(
+            exception.getMessage() + "; offending comparison: " + this, 
exception);
+      }
+    }
+
+    @Override
+    public int depth() {
+      return 1;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof Comparison)) {
+        return false;
+      }
+      Comparison that = (Comparison) other;
+      return operator == that.operator
+          && Objects.equals(left, that.left)
+          && Objects.equals(right, that.right);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(operator, left, right);
+    }
+
+    @Override
+    public String toString() {
+      return "Comparison{" + "operator=" + operator + ", left=" + left + ", 
right=" + right + '}';
+    }
+  }
+
+  /** A Boolean negation operation. */
+  final class Not implements CanonicalExpression {
+    private final CanonicalExpression operand;
+
+    Not(CanonicalExpression operand) {
+      this.operand = operand;
+    }
+
+    /**
+     * Returns the negated predicate.
+     *
+     * @return negated predicate
+     */
+    public CanonicalExpression operand() {
+      return operand;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(operand != null, "not requires an operand");
+      operand.validate();
+      Preconditions.checkArgument(
+          ExpressionValidator.isPredicate(operand), "not operand must be a 
boolean predicate");
+    }
+
+    @Override
+    public int depth() {
+      return 1 + (operand == null ? 0 : operand.depth());
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof Not && Objects.equals(operand, ((Not) 
other).operand);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(operand);
+    }
+
+    @Override
+    public String toString() {
+      return "Not{" + "operand=" + operand + '}';
+    }
+  }
+
+  /** An n-ary Boolean conjunction or disjunction. */
+  final class LogicalExpression implements CanonicalExpression {
+    private final LogicalOperator operator;
+    private final List<CanonicalExpression> operands;
+
+    LogicalExpression(LogicalOperator operator, List<CanonicalExpression> 
operands) {
+      this.operator = operator;
+      this.operands =
+          operands == null ? null : Collections.unmodifiableList(new 
ArrayList<>(operands));
+    }
+
+    static CanonicalExpression of(LogicalOperator operator, 
List<CanonicalExpression> operands) {
+      if (operands == null) {
+        return new LogicalExpression(operator, null);
+      }
+
+      List<CanonicalExpression> flattened = new ArrayList<>();
+      for (CanonicalExpression operand : operands) {
+        if (operand instanceof LogicalExpression
+            && ((LogicalExpression) operand).operator() == operator) {
+          flattened.addAll(((LogicalExpression) operand).operands());
+        } else {
+          flattened.add(operand);
+        }
+      }
+      return flattened.size() == 1 ? flattened.get(0) : new 
LogicalExpression(operator, flattened);
+    }
+
+    /**
+     * Returns the logical operator.
+     *
+     * @return logical operator
+     */
+    public LogicalOperator operator() {
+      return operator;
+    }
+
+    /**
+     * Returns the logical operands.
+     *
+     * @return immutable operands
+     */
+    public List<CanonicalExpression> operands() {
+      return operands;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(operator != null, "logical expression 
operator cannot be null");
+      Preconditions.checkArgument(
+          operands != null && operands.size() >= 2,
+          "%s requires at least two operands",
+          operator.sourceToken());
+      for (CanonicalExpression child : operands) {
+        Preconditions.checkArgument(
+            child != null, "%s operand cannot be null", 
operator.sourceToken());
+        child.validate();
+        Preconditions.checkArgument(
+            ExpressionValidator.isPredicate(child),
+            "%s operands must be boolean predicates",
+            operator.sourceToken());
+      }
+    }
+
+    @Override
+    public int depth() {
+      int childDepth = 0;
+      if (operands != null) {
+        for (CanonicalExpression child : operands) {
+          if (child != null) {
+            childDepth = Math.max(childDepth, child.depth());
+          }
+        }
+      }
+      return 1 + childDepth;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof LogicalExpression)) {
+        return false;
+      }
+      LogicalExpression that = (LogicalExpression) other;
+      return operator == that.operator && Objects.equals(operands, 
that.operands);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(operator, operands);
+    }
+
+    @Override
+    public String toString() {
+      return "LogicalExpression{" + "operator=" + operator + ", operands=" + 
operands + '}';
+    }
+  }
+
+  /** A symbolic top-level column reference. */
+  final class Column implements CanonicalExpression {
+    private final String name;
+
+    Column(String name) {
+      this.name = name;
+    }
+
+    /**
+     * Returns the exact decoded column name.
+     *
+     * @return column name
+     */
+    public String name() {
+      return name;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(name != null && !name.isEmpty(), "column 
name cannot be empty");
+      ExpressionValidator.validateUnicodeScalars(name, "column name");
+    }
+
+    @Override
+    public int depth() {
+      return 0;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof Column)) {
+        return false;
+      }
+      Column that = (Column) other;
+      return Objects.equals(name, that.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name);
+    }
+
+    @Override
+    public String toString() {
+      return "Column{" + "name='" + name + '\'' + '}';
+    }
+  }
+
+  /** A symbolic reference to the trusted effective session user. */
+  final class SessionUser implements CanonicalExpression {
+    /** Creates a session-user reference. */
+    SessionUser() {}
+
+    @Override
+    public void validate() throws IllegalArgumentException {}
+
+    @Override
+    public int depth() {
+      return 0;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof SessionUser;
+    }
+
+    @Override
+    public int hashCode() {
+      return SessionUser.class.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return "SessionUser{}";
+    }
+  }
+
+  /** A request-context group-membership predicate. */
+  final class GroupMembership implements CanonicalExpression {
+    private final String group;
+
+    GroupMembership(String group) {
+      this.group = group;
+    }
+
+    /**
+     * Returns the exact decoded group name.
+     *
+     * @return group name
+     */
+    public String group() {
+      return group;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(group != null && !group.isEmpty(), "group 
name cannot be empty");
+      ExpressionValidator.validateUnicodeScalars(group, "group name");
+    }
+
+    @Override
+    public int depth() {
+      return 1;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof GroupMembership)) {
+        return false;
+      }
+      GroupMembership that = (GroupMembership) other;
+      return Objects.equals(group, that.group);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(group);
+    }
+
+    @Override
+    public String toString() {
+      return "GroupMembership{" + "group='" + group + '\'' + '}';
+    }
+  }
+
+  /** A decoded source literal. */
+  final class Literal implements CanonicalExpression {
+    private final LiteralType literalType;
+
+    private final Object value;
+
+    private final boolean negativeZero;
+
+    Literal(LiteralType literalType, Object value) {
+      this(literalType, value, false);
+    }
+
+    Literal(LiteralType literalType, Object value, boolean negativeZero) {
+      this.literalType = literalType;
+      this.value = value;
+      this.negativeZero = negativeZero;
+    }
+
+    /**
+     * Returns the source literal type.
+     *
+     * @return literal type
+     */
+    public LiteralType literalType() {
+      return literalType;
+    }
+
+    /**
+     * Returns the decoded value.
+     *
+     * @return decoded value, or {@code null} for a null literal
+     */
+    public Object value() {
+      return value;
+    }
+
+    /**
+     * Returns whether the numeric token was a negative zero.
+     *
+     * @return {@code true} only for a numeric token with a leading minus and 
mathematical value
+     *     zero
+     */
+    public boolean negativeZero() {
+      return negativeZero;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(literalType != null, "literalType cannot be 
null");
+      switch (literalType) {
+        case STRING:
+          Preconditions.checkArgument(value instanceof String, "string literal 
must be a string");
+          Preconditions.checkArgument(!negativeZero, "string literal cannot be 
negative zero");
+          ExpressionValidator.validateUnicodeScalars((String) value, "string 
literal");
+          break;
+        case NUMBER:
+          Preconditions.checkArgument(
+              value instanceof BigDecimal, "number literal must be an exact 
decimal");
+          Preconditions.checkArgument(
+              !negativeZero || ((BigDecimal) value).signum() == 0,
+              "negative-zero marker requires a zero value");
+          break;
+        case BOOLEAN:
+          Preconditions.checkArgument(value instanceof Boolean, "boolean 
literal must be boolean");
+          Preconditions.checkArgument(!negativeZero, "boolean literal cannot 
be negative zero");
+          break;
+        case NULL:
+          Preconditions.checkArgument(value == null, "null literal cannot 
contain a value");
+          Preconditions.checkArgument(!negativeZero, "null literal cannot be 
negative zero");
+          break;
+        default:
+          throw new IllegalArgumentException("Unsupported literal type: " + 
literalType);
+      }
+    }
+
+    @Override
+    public int depth() {
+      return literalType == LiteralType.BOOLEAN ? 1 : 0;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof Literal)) {
+        return false;
+      }
+      Literal that = (Literal) other;
+      if (literalType != that.literalType || negativeZero != 
that.negativeZero) {
+        return false;
+      }
+      if (literalType == LiteralType.NUMBER) {
+        return ((BigDecimal) value).compareTo((BigDecimal) that.value) == 0;
+      }
+      return Objects.equals(value, that.value);
+    }
+
+    @Override
+    public int hashCode() {
+      Object normalizedValue =
+          literalType == LiteralType.NUMBER ? ((BigDecimal) 
value).stripTrailingZeros() : value;
+      return Objects.hash(literalType, normalizedValue, negativeZero);
+    }
+
+    @Override
+    public String toString() {
+      return "Literal{"
+          + "literalType="
+          + literalType
+          + ", value="
+          + value
+          + ", negativeZero="
+          + negativeZero
+          + '}';
+    }
+  }
+
+  /** A non-empty homogeneous array of non-null source literals. */
+  final class LiteralArray implements CanonicalExpression {
+    private final LiteralType elementType;
+
+    private final List<Literal> values;
+
+    LiteralArray(LiteralType elementType, List<Literal> values) {
+      this.elementType = elementType;
+      this.values = values == null ? null : Collections.unmodifiableList(new 
ArrayList<>(values));
+    }
+
+    /**
+     * Returns the common element type.
+     *
+     * @return element type
+     */
+    public LiteralType elementType() {
+      return elementType;
+    }
+
+    /**
+     * Returns the literal values.
+     *
+     * @return immutable literal values
+     */
+    public List<Literal> values() {
+      return values;
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          elementType != null && elementType != LiteralType.NULL,
+          "array element type must be non-null");
+      Preconditions.checkArgument(values != null && !values.isEmpty(), "array 
cannot be empty");
+      for (Literal value : values) {
+        Preconditions.checkArgument(value != null, "array cannot contain a 
null node");
+        value.validate();
+        Preconditions.checkArgument(
+            value.literalType() == elementType,
+            "array literals must have one homogeneous non-null type");
+      }
+    }
+
+    @Override
+    public int depth() {
+      return 0;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof LiteralArray)) {
+        return false;
+      }
+      LiteralArray that = (LiteralArray) other;
+      return elementType == that.elementType && Objects.equals(values, 
that.values);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(elementType, values);
+    }
+
+    @Override
+    public String toString() {
+      return "LiteralArray{" + "elementType=" + elementType + ", values=" + 
values + '}';
+    }
+  }
+}

Review Comment:
   Addressed in 8d046c5dc9. `CanonicalExpression.java` now contains only the 
49-line expression contract. Each operator/type and AST node (`Comparison`, 
`Not`, `LogicalExpression`, `Column`, `SessionUser`, `GroupMembership`, 
`Literal`, and `LiteralArray`) lives in its own package-private file. This is a 
structural refactor only; the parser golden suite and full parser tests pass 
unchanged.



##########
core/src/main/java/org/apache/gravitino/policy/rego/RestrictedRegoExpressionParserFacade.java:
##########
@@ -0,0 +1,819 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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 org.apache.gravitino.policy.rego;
+
+import com.google.common.base.Preconditions;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.policy.ReadRestrictionContent;
+import org.apache.gravitino.policy.rego.CanonicalExpression.Comparison;
+import org.apache.gravitino.policy.rego.CanonicalExpression.ComparisonOperator;
+import org.apache.gravitino.policy.rego.CanonicalExpression.GroupMembership;
+import org.apache.gravitino.policy.rego.CanonicalExpression.Literal;
+import org.apache.gravitino.policy.rego.CanonicalExpression.LiteralArray;
+import org.apache.gravitino.policy.rego.CanonicalExpression.LiteralType;
+import org.apache.gravitino.policy.rego.CanonicalExpression.LogicalExpression;
+import org.apache.gravitino.policy.rego.CanonicalExpression.LogicalOperator;
+import org.apache.gravitino.policy.rego.CanonicalExpression.Not;
+import org.apache.gravitino.policy.rego.CanonicalExpression.SessionUser;
+import org.apache.gravitino.policy.rego.RestrictedRegoProgram.ColumnMask;
+import org.apache.gravitino.policy.rego.RestrictedRegoProgram.FilterBranch;
+import org.apache.gravitino.policy.rego.RestrictedRegoProgram.MaskAction;
+import org.apache.gravitino.policy.rego.RestrictedRegoProgram.MaskBranch;
+import org.apache.gravitino.policy.rego.RestrictedRegoProgram.RowFilter;
+
+/** Parses and validates complete programs in the {@code restricted-rego-v1} 
source dialect. */
+public final class RestrictedRegoExpressionParserFacade {
+
+  private static final int MAX_SOURCE_DEPTH = 8;
+  private static final int MAX_AST_NODES = 256;
+  private static final int MAX_STRING_BYTES = 4 * 1024;
+  private static final int MAX_ARRAY_ELEMENTS = 256;
+  private static final int MAX_TOTAL_ARRAY_ELEMENTS = 256;
+  private static final int MAX_NUMERIC_LITERAL_BYTES = 256;
+
+  private RestrictedRegoExpressionParserFacade() {}
+
+  /**
+   * Parses and validates one complete row-filter or column-mask program.
+   *
+   * @param source program in {@code restricted-rego-v1}
+   * @return validated unresolved program
+   * @throws IllegalArgumentException if syntax or semantics are invalid
+   */
+  public static RestrictedRegoProgram parse(String source) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(source), "restricted-rego-v1 program cannot be 
blank");
+    Preconditions.checkArgument(
+        source.getBytes(StandardCharsets.UTF_8).length
+            <= ReadRestrictionContent.MAX_SOURCE_LENGTH_BYTES,
+        "restricted-rego-v1 source must not exceed %s UTF-8 bytes",
+        ReadRestrictionContent.MAX_SOURCE_LENGTH_BYTES);
+
+    RestrictedRegoProgram program = new SourceParser(source).parseProgram();
+    validateProgram(program);
+    return program;
+  }
+
+  /**
+   * Parses and validates one complete {@code filter := ...} program.
+   *
+   * @param source row-filter program in {@code restricted-rego-v1}
+   * @return validated unresolved row-filter rule
+   * @throws IllegalArgumentException if syntax or semantics are invalid or 
the rule head is not
+   *     {@code filter}
+   */
+  public static RowFilter parseRowFilter(String source) {
+    RestrictedRegoProgram program = parse(source);
+    Preconditions.checkArgument(
+        program instanceof RowFilter, "row-filter program must declare a 
filter rule");
+    return (RowFilter) program;
+  }
+
+  /**
+   * Parses and validates one complete {@code mask := action(...)} program.
+   *
+   * @param source column-mask program in {@code restricted-rego-v1}
+   * @return validated unresolved column-mask rule
+   * @throws IllegalArgumentException if syntax or semantics are invalid or 
the rule head is not
+   *     {@code mask}
+   */
+  public static ColumnMask parseColumnMask(String source) {
+    RestrictedRegoProgram program = parse(source);
+    Preconditions.checkArgument(
+        program instanceof ColumnMask, "column-mask program must declare a 
mask rule");
+    return (ColumnMask) program;
+  }
+
+  private static void validateProgram(RestrictedRegoProgram program) {
+    program.validate();
+    List<CanonicalExpression> expressions = expressions(program);
+    int nodeCount = 0;
+    int arrayElementCount = 0;
+    for (CanonicalExpression expression : expressions) {
+      Preconditions.checkArgument(
+          expression.depth() <= MAX_SOURCE_DEPTH,
+          "restricted-rego-v1 source depth must not exceed %s",
+          MAX_SOURCE_DEPTH);
+      nodeCount += countNodes(expression);
+      arrayElementCount += countArrayElements(expression);
+    }
+    Preconditions.checkArgument(
+        nodeCount <= MAX_AST_NODES,
+        "restricted-rego-v1 AST must not exceed %s nodes",
+        MAX_AST_NODES);
+    Preconditions.checkArgument(
+        arrayElementCount <= MAX_TOTAL_ARRAY_ELEMENTS,
+        "restricted-rego-v1 literal arrays must not contain more than %s total 
elements",
+        MAX_TOTAL_ARRAY_ELEMENTS);
+    if (program instanceof RowFilter) {
+      CanonicalExpression lowered = ((RowFilter) program).lower();
+      Preconditions.checkArgument(
+          lowered.depth() <= MAX_SOURCE_DEPTH,
+          "lowered row-filter depth must not exceed %s",
+          MAX_SOURCE_DEPTH);
+      Preconditions.checkArgument(
+          countNodes(lowered) <= MAX_AST_NODES,
+          "lowered row-filter AST must not exceed %s nodes",
+          MAX_AST_NODES);
+      Preconditions.checkArgument(
+          countArrayElements(lowered) <= MAX_TOTAL_ARRAY_ELEMENTS,
+          "lowered row-filter literal arrays must not contain more than %s 
total elements",
+          MAX_TOTAL_ARRAY_ELEMENTS);
+    }
+  }
+
+  private static List<CanonicalExpression> expressions(RestrictedRegoProgram 
program) {
+    List<CanonicalExpression> expressions = new ArrayList<>();
+    if (program instanceof RowFilter) {
+      RowFilter filter = (RowFilter) program;
+      for (FilterBranch branch : filter.branches()) {
+        expressions.add(branch.result());
+        expressions.add(branch.condition());
+      }
+      expressions.add(filter.fallback());
+    } else {
+      ColumnMask mask = (ColumnMask) program;
+      for (MaskBranch branch : mask.branches()) {
+        expressions.add(branch.condition());
+      }
+    }
+    return expressions;
+  }
+
+  private static int countNodes(CanonicalExpression expression) {
+    if (expression instanceof Comparison) {
+      Comparison comparison = (Comparison) expression;
+      return 1 + countNodes(comparison.left()) + 
countNodes(comparison.right());
+    }
+    if (expression instanceof Not) {
+      return 1 + countNodes(((Not) expression).operand());
+    }
+    if (expression instanceof LogicalExpression) {
+      int count = 1;
+      for (CanonicalExpression child : ((LogicalExpression) 
expression).operands()) {
+        count += countNodes(child);
+      }
+      return count;
+    }
+    return 1;
+  }
+
+  private static int countArrayElements(CanonicalExpression expression) {
+    if (expression instanceof Comparison) {
+      Comparison comparison = (Comparison) expression;
+      return countArrayElements(comparison.left()) + 
countArrayElements(comparison.right());
+    }
+    if (expression instanceof Not) {
+      return countArrayElements(((Not) expression).operand());
+    }
+    if (expression instanceof LogicalExpression) {
+      int count = 0;
+      for (CanonicalExpression child : ((LogicalExpression) 
expression).operands()) {
+        count += countArrayElements(child);
+      }
+      return count;
+    }
+    if (expression instanceof LiteralArray) {
+      return ((LiteralArray) expression).values().size();
+    }
+    return 0;
+  }
+
+  private static boolean isAsciiHexDigit(char value) {
+    return (value >= '0' && value <= '9')
+        || (value >= 'a' && value <= 'f')
+        || (value >= 'A' && value <= 'F');
+  }
+
+  private static int asciiHexValue(char value) {
+    if (value >= '0' && value <= '9') {
+      return value - '0';
+    }
+    if (value >= 'a' && value <= 'f') {
+      return value - 'a' + 10;
+    }
+    return value - 'A' + 10;
+  }
+
+  private static final class SourceParser {
+    private static final int MAX_PARENTHESIS_NESTING = 256;
+
+    private final Lexer lexer;
+    private Token current;
+    private int parenthesisNesting;
+
+    private SourceParser(String source) {
+      this.lexer = new Lexer(source);
+      this.current = lexer.nextToken();
+    }
+
+    private RestrictedRegoProgram parseProgram() {
+      RestrictedRegoProgram program;
+      if (check(TokenType.FILTER)) {
+        program = parseRowFilter();
+      } else if (check(TokenType.MASK)) {
+        program = parseColumnMask();
+      } else {
+        throw error("expected filter or mask rule");
+      }
+      expect(TokenType.EOF, "expected end of program");
+      return program;
+    }
+
+    private RowFilter parseRowFilter() {
+      expect(TokenType.FILTER, "expected filter");
+      expect(TokenType.ASSIGN, "expected := after filter");
+      CanonicalExpression result = parseExpression();
+      if (!match(TokenType.IF)) {
+        return new RowFilter(new ArrayList<>(), result);
+      }
+
+      List<FilterBranch> branches = new ArrayList<>();
+      branches.add(new FilterBranch(result, parseExpression()));
+      while (true) {
+        expect(TokenType.ELSE, "expected final else branch");
+        expect(TokenType.ASSIGN, "expected := after else");
+        CanonicalExpression nextResult = parseExpression();
+        if (!match(TokenType.IF)) {
+          return new RowFilter(branches, nextResult);
+        }
+        branches.add(new FilterBranch(nextResult, parseExpression()));
+      }
+    }
+
+    private ColumnMask parseColumnMask() {
+      expect(TokenType.MASK, "expected mask");
+      expect(TokenType.ASSIGN, "expected := after mask");
+      MaskAction action = parseMaskAction();
+      if (!match(TokenType.IF)) {
+        return new ColumnMask(new ArrayList<>(), action);
+      }
+
+      List<MaskBranch> branches = new ArrayList<>();
+      branches.add(new MaskBranch(action, parseExpression()));
+      while (true) {
+        expect(TokenType.ELSE, "expected final else branch");
+        expect(TokenType.ASSIGN, "expected := after else");
+        MaskAction nextAction = parseMaskAction();
+        if (!match(TokenType.IF)) {
+          return new ColumnMask(branches, nextAction);
+        }
+        branches.add(new MaskBranch(nextAction, parseExpression()));
+      }
+    }
+
+    private MaskAction parseMaskAction() {
+      expect(TokenType.ACTION, "expected action");
+      expect(TokenType.LPAREN, "expected ( after action");
+      Token actionToken = expect(TokenType.STRING, "expected mask action 
string");
+      String action = decodeString(actionToken);
+      expect(TokenType.RPAREN, "expected ) after mask action");
+      try {
+        return MaskAction.fromValue(action);
+      } catch (IllegalArgumentException exception) {
+        throw error(actionToken, exception.getMessage());
+      }
+    }
+
+    private CanonicalExpression parseExpression() {
+      return parseOrExpression();
+    }
+
+    private CanonicalExpression parseOrExpression() {
+      List<CanonicalExpression> operands = new ArrayList<>();
+      operands.add(parseAndExpression());
+      while (match(TokenType.OR)) {
+        operands.add(parseAndExpression());
+      }
+      return LogicalExpression.of(LogicalOperator.OR, operands);
+    }
+
+    private CanonicalExpression parseAndExpression() {
+      List<CanonicalExpression> operands = new ArrayList<>();
+      operands.add(parseNotExpression());
+      while (match(TokenType.AND)) {
+        operands.add(parseNotExpression());
+      }
+      return LogicalExpression.of(LogicalOperator.AND, operands);
+    }
+
+    private CanonicalExpression parseNotExpression() {
+      int notCount = 0;
+      while (match(TokenType.NOT)) {
+        notCount++;
+      }
+
+      CanonicalExpression result = parseComparisonExpression();
+      for (int index = 0; index < notCount; index++) {
+        result = new Not(result);
+      }
+      return result;
+    }
+
+    private CanonicalExpression parseComparisonExpression() {
+      CanonicalExpression left = parsePrimary();
+      if (!isComparisonOperator(current.type)) {
+        return left;
+      }
+
+      ComparisonOperator operator = 
ComparisonOperator.fromSourceToken(advance().text);
+      CanonicalExpression right = parsePrimary();
+      if (isComparisonOperator(current.type)) {
+        throw error("chained comparisons are not supported");
+      }
+      return new Comparison(operator, left, right);
+    }
+
+    private CanonicalExpression parsePrimary() {
+      if (match(TokenType.COL)) {
+        expect(TokenType.LPAREN, "expected ( after col");
+        String column = decodeString(expect(TokenType.STRING, "expected column 
name string"));
+        expect(TokenType.RPAREN, "expected ) after column name");
+        return new CanonicalExpression.Column(column);
+      }
+      if (match(TokenType.SESSION_USER)) {
+        expect(TokenType.LPAREN, "expected ( after session_user");
+        expect(TokenType.RPAREN, "expected ) after session_user(");
+        return new SessionUser();
+      }
+      if (match(TokenType.IS_GROUP_MEMBER)) {
+        expect(TokenType.LPAREN, "expected ( after is_group_member");
+        String group = decodeString(expect(TokenType.STRING, "expected group 
name string"));
+        expect(TokenType.RPAREN, "expected ) after group name");
+        return new GroupMembership(group);
+      }
+      if (isLiteral(current.type)) {
+        return parseLiteral();
+      }
+      if (match(TokenType.LBRACKET)) {
+        return parseLiteralArray();
+      }
+      if (match(TokenType.LPAREN)) {
+        parenthesisNesting++;
+        if (parenthesisNesting > MAX_PARENTHESIS_NESTING) {
+          throw error("parenthesis nesting must not exceed " + 
MAX_PARENTHESIS_NESTING);
+        }
+        CanonicalExpression expression = parseExpression();
+        expect(TokenType.RPAREN, "expected ) after expression");
+        parenthesisNesting--;
+        return expression;
+      }
+      throw error("expected expression");
+    }
+
+    private Literal parseLiteral() {
+      Token token = advance();
+      switch (token.type) {
+        case STRING:
+          return new Literal(LiteralType.STRING, decodeString(token));
+        case NUMBER:
+          BigDecimal value = new BigDecimal(token.text);
+          return new Literal(
+              LiteralType.NUMBER, value, token.text.startsWith("-") && 
value.signum() == 0);
+        case TRUE:
+          return new Literal(LiteralType.BOOLEAN, true);
+        case FALSE:
+          return new Literal(LiteralType.BOOLEAN, false);
+        case NULL:
+          return new Literal(LiteralType.NULL, null);
+        default:
+          throw error(token, "expected literal");
+      }
+    }
+
+    private LiteralArray parseLiteralArray() {
+      List<Literal> values = new ArrayList<>();
+      LiteralType elementType = null;
+      do {
+        if (values.size() >= MAX_ARRAY_ELEMENTS) {
+          throw error("array literal must not exceed " + MAX_ARRAY_ELEMENTS + 
" elements");
+        }
+        if (!isLiteral(current.type)) {
+          throw error("expected a literal array element");
+        }
+        Literal literal = parseLiteral();
+        if (elementType == null) {
+          elementType = literal.literalType();
+        }
+        values.add(literal);
+      } while (match(TokenType.COMMA));
+      expect(TokenType.RBRACKET, "expected ] after array literal");
+      return new LiteralArray(elementType, values);
+    }
+
+    private boolean match(TokenType type) {
+      if (!check(type)) {
+        return false;
+      }
+      advance();
+      return true;
+    }
+
+    private boolean check(TokenType type) {
+      return current.type == type;
+    }
+
+    private Token expect(TokenType type, String message) {
+      if (!check(type)) {
+        throw error(message);
+      }
+      return advance();
+    }
+
+    private Token advance() {
+      Token previous = current;
+      current = lexer.nextToken();
+      return previous;
+    }
+
+    private IllegalArgumentException error(String message) {
+      return error(current, message);
+    }
+
+    private static IllegalArgumentException error(Token token, String message) 
{
+      return new IllegalArgumentException(
+          String.format(
+              "Invalid restricted-rego-v1 program at line %s, column %s: %s",
+              token.line, token.column, message));
+    }
+
+    private static boolean isComparisonOperator(TokenType type) {
+      return type == TokenType.EQ
+          || type == TokenType.NEQ
+          || type == TokenType.LT
+          || type == TokenType.LTE
+          || type == TokenType.GT
+          || type == TokenType.GTE
+          || type == TokenType.IN;
+    }
+
+    private static boolean isLiteral(TokenType type) {
+      return type == TokenType.STRING
+          || type == TokenType.NUMBER
+          || type == TokenType.TRUE
+          || type == TokenType.FALSE
+          || type == TokenType.NULL;
+    }
+
+    private static String decodeString(Token token) {
+      StringBuilder result = new StringBuilder();
+      for (int index = 1; index < token.text.length() - 1; index++) {
+        char current = token.text.charAt(index);
+        if (current != '\\') {
+          result.append(current);
+          continue;
+        }
+
+        char escaped = token.text.charAt(++index);
+        switch (escaped) {
+          case '"':
+          case '\\':
+          case '/':
+            result.append(escaped);
+            break;
+          case 'b':
+            result.append('\b');
+            break;
+          case 'f':
+            result.append('\f');
+            break;
+          case 'n':
+            result.append('\n');
+            break;
+          case 'r':
+            result.append('\r');
+            break;
+          case 't':
+            result.append('\t');
+            break;
+          case 'u':
+            int codeUnit = 0;
+            for (int digitIndex = index + 1; digitIndex < index + 5; 
digitIndex++) {
+              char digit = token.text.charAt(digitIndex);
+              if (!isAsciiHexDigit(digit)) {
+                throw error(token, "invalid Unicode escape in string");
+              }
+              codeUnit = codeUnit * 16 + asciiHexValue(digit);
+            }
+            result.append((char) codeUnit);
+            index += 4;
+            break;
+          default:
+            throw error(token, "unsupported string escape: \\" + escaped);
+        }
+      }
+      String decoded = result.toString();
+      if (decoded.getBytes(StandardCharsets.UTF_8).length > MAX_STRING_BYTES) {
+        throw error(
+            token, "decoded string literal must not exceed " + 
MAX_STRING_BYTES + " UTF-8 bytes");
+      }
+      return decoded;
+    }
+  }
+
+  private enum TokenType {
+    FILTER,
+    MASK,
+    IF,
+    ELSE,
+    OR,
+    AND,
+    NOT,
+    IN,
+    COL,
+    SESSION_USER,
+    IS_GROUP_MEMBER,
+    ACTION,
+    TRUE,
+    FALSE,
+    NULL,
+    ASSIGN,
+    EQ,
+    NEQ,
+    LTE,
+    LT,
+    GTE,
+    GT,
+    LPAREN,
+    RPAREN,
+    LBRACKET,
+    RBRACKET,
+    COMMA,
+    NUMBER,
+    STRING,
+    EOF
+  }
+
+  private static final class Token {
+    private final TokenType type;
+    private final String text;
+    private final int line;
+    private final int column;
+
+    private Token(TokenType type, String text, int line, int column) {
+      this.type = type;
+      this.text = text;
+      this.line = line;
+      this.column = column;
+    }
+  }
+
+  private static final class Lexer {
+    private final String source;
+    private int offset;
+    private int line = 1;
+    private int column = 1;
+
+    private Lexer(String source) {
+      this.source = source;
+    }
+
+    private Token nextToken() {
+      skipWhitespace();
+      int tokenOffset = offset;
+      int tokenLine = line;
+      int tokenColumn = column;
+      if (atEnd()) {
+        return new Token(TokenType.EOF, "", tokenLine, tokenColumn);
+      }
+
+      char current = peek();
+      if (current == '"') {
+        scanString(tokenLine, tokenColumn);
+        return token(TokenType.STRING, tokenOffset, tokenLine, tokenColumn);
+      }
+      if (isIdentifierStart(current)) {
+        do {
+          advance();
+        } while (!atEnd() && isIdentifierPart(peek()));
+        String text = source.substring(tokenOffset, offset);
+        return new Token(keywordType(text, tokenLine, tokenColumn), text, 
tokenLine, tokenColumn);
+      }
+      if (isDigit(current) || (current == '-' && isDigit(peekNext()))) {
+        scanNumber(tokenLine, tokenColumn);
+        if (offset - tokenOffset > MAX_NUMERIC_LITERAL_BYTES) {
+          throw lexicalError(
+              tokenLine,
+              tokenColumn,
+              "numeric literal must not exceed " + MAX_NUMERIC_LITERAL_BYTES + 
" bytes");
+        }
+        if (!atEnd() && isIdentifierStart(peek())) {
+          throw lexicalError(
+              tokenLine, tokenColumn, "numeric literal must be separated from 
identifiers");
+        }
+        return token(TokenType.NUMBER, tokenOffset, tokenLine, tokenColumn);
+      }
+
+      advance();
+      switch (current) {
+        case ':':
+          return symbol(TokenType.ASSIGN, '=', tokenOffset, tokenLine, 
tokenColumn);
+        case '=':
+          return symbol(TokenType.EQ, '=', tokenOffset, tokenLine, 
tokenColumn);
+        case '!':
+          return symbol(TokenType.NEQ, '=', tokenOffset, tokenLine, 
tokenColumn);
+        case '<':
+          return optionalEquals(TokenType.LT, TokenType.LTE, tokenOffset, 
tokenLine, tokenColumn);
+        case '>':
+          return optionalEquals(TokenType.GT, TokenType.GTE, tokenOffset, 
tokenLine, tokenColumn);
+        case '(':
+          return token(TokenType.LPAREN, tokenOffset, tokenLine, tokenColumn);
+        case ')':
+          return token(TokenType.RPAREN, tokenOffset, tokenLine, tokenColumn);
+        case '[':
+          return token(TokenType.LBRACKET, tokenOffset, tokenLine, 
tokenColumn);
+        case ']':
+          return token(TokenType.RBRACKET, tokenOffset, tokenLine, 
tokenColumn);
+        case ',':
+          return token(TokenType.COMMA, tokenOffset, tokenLine, tokenColumn);
+        default:
+          throw lexicalError(tokenLine, tokenColumn, "unsupported character " 
+ current);
+      }
+    }
+
+    private void scanString(int tokenLine, int tokenColumn) {
+      advance();
+      while (!atEnd()) {
+        char current = advance();
+        if (current == '"') {
+          return;
+        }
+        if (current < 0x20) {
+          throw lexicalError(tokenLine, tokenColumn, "unescaped control 
character in string");
+        }
+        if (current != '\\') {
+          continue;
+        }
+        if (atEnd()) {
+          break;
+        }
+        char escaped = advance();
+        if (escaped == 'u') {
+          for (int index = 0; index < 4; index++) {
+            if (atEnd() || !isAsciiHexDigit(advance())) {
+              throw lexicalError(tokenLine, tokenColumn, "invalid Unicode 
escape in string");
+            }
+          }
+        } else if ("\"\\/bfnrt".indexOf(escaped) < 0) {
+          throw lexicalError(tokenLine, tokenColumn, "unsupported string 
escape");
+        }
+      }
+      throw lexicalError(tokenLine, tokenColumn, "unterminated string");
+    }
+
+    private void scanNumber(int tokenLine, int tokenColumn) {
+      if (peek() == '-') {
+        advance();
+      }
+      if (peek() == '0') {
+        advance();
+        if (!atEnd() && isDigit(peek())) {
+          throw lexicalError(
+              tokenLine, tokenColumn, "numeric literals cannot contain leading 
zeros");
+        }
+      } else {
+        while (!atEnd() && isDigit(peek())) {
+          advance();
+        }
+      }
+      if (!atEnd() && peek() == '.' && isDigit(peekNext())) {
+        advance();
+        while (!atEnd() && isDigit(peek())) {
+          advance();
+        }
+      }
+    }
+
+    private Token symbol(
+        TokenType type, char second, int tokenOffset, int tokenLine, int 
tokenColumn) {
+      if (atEnd() || peek() != second) {
+        throw lexicalError(tokenLine, tokenColumn, "unsupported operator");
+      }
+      advance();
+      return token(type, tokenOffset, tokenLine, tokenColumn);
+    }
+
+    private Token optionalEquals(
+        TokenType single, TokenType withEquals, int tokenOffset, int 
tokenLine, int tokenColumn) {
+      TokenType type = single;
+      if (!atEnd() && peek() == '=') {
+        advance();
+        type = withEquals;
+      }
+      return token(type, tokenOffset, tokenLine, tokenColumn);
+    }
+
+    private Token token(TokenType type, int tokenOffset, int tokenLine, int 
tokenColumn) {
+      return new Token(type, source.substring(tokenOffset, offset), tokenLine, 
tokenColumn);
+    }
+
+    private void skipWhitespace() {
+      while (!atEnd()) {
+        char current = peek();
+        if (current != ' ' && current != '\t' && current != '\r' && current != 
'\n') {
+          return;
+        }
+        advance();
+      }
+    }
+
+    private char advance() {
+      char current = source.charAt(offset++);
+      if (current == '\n') {
+        line++;
+        column = 1;
+      } else {
+        column++;
+      }
+      return current;
+    }
+
+    private char peek() {
+      return source.charAt(offset);
+    }
+
+    private char peekNext() {
+      return offset + 1 < source.length() ? source.charAt(offset + 1) : '\0';
+    }
+
+    private boolean atEnd() {
+      return offset >= source.length();
+    }
+
+    private static TokenType keywordType(String text, int line, int column) {
+      switch (text) {
+        case "filter":
+          return TokenType.FILTER;
+        case "mask":
+          return TokenType.MASK;
+        case "if":
+          return TokenType.IF;
+        case "else":
+          return TokenType.ELSE;
+        case "or":
+          return TokenType.OR;
+        case "and":
+          return TokenType.AND;
+        case "not":
+          return TokenType.NOT;
+        case "in":
+          return TokenType.IN;
+        case "col":
+          return TokenType.COL;
+        case "session_user":
+          return TokenType.SESSION_USER;
+        case "is_group_member":
+          return TokenType.IS_GROUP_MEMBER;
+        case "action":
+          return TokenType.ACTION;
+        case "true":
+          return TokenType.TRUE;
+        case "false":
+          return TokenType.FALSE;
+        case "null":
+          return TokenType.NULL;
+        default:
+          throw lexicalError(line, column, "unsupported identifier " + text);
+      }
+    }
+
+    private static boolean isIdentifierStart(char value) {
+      return (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') 
|| value == '_';
+    }
+
+    private static boolean isIdentifierPart(char value) {
+      return isIdentifierStart(value) || isDigit(value);
+    }
+
+    private static boolean isDigit(char value) {
+      return value >= '0' && value <= '9';
+    }
+
+    private static IllegalArgumentException lexicalError(int line, int column, 
String message) {
+      return new IllegalArgumentException(
+          String.format(
+              "Invalid restricted-rego-v1 program at line %s, column %s: %s",
+              line, column, message));
+    }
+  }
+}

Review Comment:
   Addressed in 8d046c5dc9. The facade now contains only the public parse entry 
points. Lexing, source parsing, source resource validation, and limits were 
split into `RestrictedRegoLexer`, `RestrictedRegoSourceParser`, 
`RestrictedRegoSourceValidator`, and `RestrictedRegoLimits`. Parser/golden 
tests, PolicyManager tests, Spotless, and core Javadoc all pass.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to