diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java index b0b914b1..a84ef076 100644 --- a/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java @@ -191,6 +191,7 @@ private void buildValue( processEnumConstraints(fieldDescriptor, fieldConstraints, valueEvaluator); processMapConstraints(fieldDescriptor, fieldConstraints, valueEvaluator); processRepeatedConstraints(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + processTimestampConstraints(fieldDescriptor, fieldConstraints, valueEvaluator); } private void processFieldExpressions( @@ -360,6 +361,21 @@ private void processRepeatedConstraints( valueEvaluatorEval.append(listEval); } + private void processTimestampConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + ValueEvaluator valueEvaluatorEval) { + if (fieldDescriptor.getType() != FieldDescriptor.Type.MESSAGE + || !fieldDescriptor.getMessageType().getFullName().equals("google.protobuf.Timestamp")) { + return; + } + FieldDescriptor secondsDesc = fieldDescriptor.getMessageType().findFieldByName("seconds"); + FieldDescriptor nanosDesc = fieldDescriptor.getMessageType().findFieldByName("nanos"); + TimestampEvaluator timestampEvaluatorEval = + new TimestampEvaluator(secondsDesc, nanosDesc, fieldConstraints.getTimestamp().getValid()); + valueEvaluatorEval.append(timestampEvaluatorEval); + } + private static List compileConstraints(List constraints, Env env) throws CompilationException { List expressions = Expression.fromConstraints(constraints); diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/TimestampEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/TimestampEvaluator.java new file mode 100644 index 00000000..274d2cfa --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/TimestampEvaluator.java @@ -0,0 +1,88 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// 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 +// +// 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 build.buf.protovalidate.internal.evaluator; + +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import build.buf.validate.Violation; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Message; +import java.util.ArrayList; +import java.util.List; + +/** + * A specialized evaluator for applying some {@link build.buf.validate.TimestampRules} (only the + * `valid` rule currently) to an {@link com.google.protobuf.Timestamp} message. This is handled + * outside CEL which handles {@link com.google.protobuf.Timestamp} as an abstract type, thus not + * allowing access to the message fields. + */ +class TimestampEvaluator implements Evaluator { + private final long maxTimestamp = +253402300799L; + private final long minTimestamp = -62135596800L; + + private final Descriptors.FieldDescriptor secondsDescriptor; + private final Descriptors.FieldDescriptor nanosDescriptor; + private final boolean valid; + + /** Constructs a new evaluator for {@link build.buf.validate.TimestampRules} messages. */ + TimestampEvaluator( + Descriptors.FieldDescriptor secondsDescriptor, + Descriptors.FieldDescriptor nanosDescriptor, + boolean valid) { + this.secondsDescriptor = secondsDescriptor; + this.nanosDescriptor = nanosDescriptor; + this.valid = valid; + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + Message timestampValue = val.messageValue(); + if (timestampValue == null) { + return ValidationResult.EMPTY; + } + List violationList = new ArrayList<>(); + if (valid) { + long seconds = (long) timestampValue.getField(secondsDescriptor); + int nanos = (int) timestampValue.getField(nanosDescriptor); + + String errorMessage = ""; + if (seconds < minTimestamp) { + errorMessage = "timestamp before 0001-01-01"; + } else if (seconds > maxTimestamp) { + errorMessage = "timestamp after 9999-12-31"; + } else if (nanos < 0 || nanos >= 1e9) { + errorMessage = "timestamp has out-of-range nanos"; + } + + if (errorMessage.length() != 0) { + Violation violation = + Violation.newBuilder() + .setConstraintId("timestamp.valid") + .setMessage(errorMessage) + .build(); + violationList.add(violation); + if (failFast) { + return new ValidationResult(violationList); + } + } + } + return new ValidationResult(violationList); + } + + @Override + public boolean tautology() { + return !valid; + } +} diff --git a/src/main/java/build/buf/validate/TimestampRules.java b/src/main/java/build/buf/validate/TimestampRules.java index 23fb735e..9441405c 100644 --- a/src/main/java/build/buf/validate/TimestampRules.java +++ b/src/main/java/build/buf/validate/TimestampRules.java @@ -651,6 +651,21 @@ public com.google.protobuf.DurationOrBuilder getWithinOrBuilder() { return within_ == null ? com.google.protobuf.Duration.getDefaultInstance() : within_; } + public static final int VALID_FIELD_NUMBER = 10; + private boolean valid_ = false; + /** + *
+   * some comments
+   * 
+ * + * bool valid = 10 [json_name = "valid"]; + * @return The valid. + */ + @java.lang.Override + public boolean getValid() { + return valid_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override public final boolean isInitialized() { @@ -691,6 +706,9 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) if (((bitField0_ & 0x00000002) != 0)) { output.writeMessage(9, getWithin()); } + if (valid_ != false) { + output.writeBool(10, valid_); + } getUnknownFields().writeTo(output); } @@ -734,6 +752,10 @@ public int getSerializedSize() { size += com.google.protobuf.CodedOutputStream .computeMessageSize(9, getWithin()); } + if (valid_ != false) { + size += com.google.protobuf.CodedOutputStream + .computeBoolSize(10, valid_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -759,6 +781,8 @@ public boolean equals(final java.lang.Object obj) { if (!getWithin() .equals(other.getWithin())) return false; } + if (getValid() + != other.getValid()) return false; if (!getLessThanCase().equals(other.getLessThanCase())) return false; switch (lessThanCase_) { case 3: @@ -812,6 +836,9 @@ public int hashCode() { hash = (37 * hash) + WITHIN_FIELD_NUMBER; hash = (53 * hash) + getWithin().hashCode(); } + hash = (37 * hash) + VALID_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean( + getValid()); switch (lessThanCase_) { case 3: hash = (37 * hash) + LT_FIELD_NUMBER; @@ -1010,6 +1037,7 @@ public Builder clear() { withinBuilder_.dispose(); withinBuilder_ = null; } + valid_ = false; lessThanCase_ = 0; lessThan_ = null; greaterThanCase_ = 0; @@ -1061,6 +1089,9 @@ private void buildPartial0(build.buf.validate.TimestampRules result) { : withinBuilder_.build(); to_bitField0_ |= 0x00000002; } + if (((from_bitField0_ & 0x00000100) != 0)) { + result.valid_ = valid_; + } result.bitField0_ |= to_bitField0_; } @@ -1137,6 +1168,9 @@ public Builder mergeFrom(build.buf.validate.TimestampRules other) { if (other.hasWithin()) { mergeWithin(other.getWithin()); } + if (other.getValid() != false) { + setValid(other.getValid()); + } switch (other.getLessThanCase()) { case LT: { mergeLt(other.getLt()); @@ -1249,6 +1283,11 @@ public Builder mergeFrom( bitField0_ |= 0x00000080; break; } // case 74 + case 80: { + valid_ = input.readBool(); + bitField0_ |= 0x00000100; + break; + } // case 80 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { done = true; // was an endgroup tag @@ -3051,6 +3090,50 @@ public com.google.protobuf.DurationOrBuilder getWithinOrBuilder() { } return withinBuilder_; } + + private boolean valid_ ; + /** + *
+     * some comments
+     * 
+ * + * bool valid = 10 [json_name = "valid"]; + * @return The valid. + */ + @java.lang.Override + public boolean getValid() { + return valid_; + } + /** + *
+     * some comments
+     * 
+ * + * bool valid = 10 [json_name = "valid"]; + * @param value The valid to set. + * @return This builder for chaining. + */ + public Builder setValid(boolean value) { + + valid_ = value; + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + *
+     * some comments
+     * 
+ * + * bool valid = 10 [json_name = "valid"]; + * @return This builder for chaining. + */ + public Builder clearValid() { + bitField0_ = (bitField0_ & ~0x00000100); + valid_ = false; + onChanged(); + return this; + } @java.lang.Override public final Builder setUnknownFields( final com.google.protobuf.UnknownFieldSet unknownFields) { diff --git a/src/main/java/build/buf/validate/TimestampRulesOrBuilder.java b/src/main/java/build/buf/validate/TimestampRulesOrBuilder.java index f4cd1451..0454f163 100644 --- a/src/main/java/build/buf/validate/TimestampRulesOrBuilder.java +++ b/src/main/java/build/buf/validate/TimestampRulesOrBuilder.java @@ -421,6 +421,16 @@ public interface TimestampRulesOrBuilder extends */ com.google.protobuf.DurationOrBuilder getWithinOrBuilder(); + /** + *
+   * some comments
+   * 
+ * + * bool valid = 10 [json_name = "valid"]; + * @return The valid. + */ + boolean getValid(); + build.buf.validate.TimestampRules.LessThanCase getLessThanCase(); build.buf.validate.TimestampRules.GreaterThanCase getGreaterThanCase(); diff --git a/src/main/java/build/buf/validate/ValidateProto.java b/src/main/java/build/buf/validate/ValidateProto.java index c2333a3a..84319a23 100644 --- a/src/main/java/build/buf/validate/ValidateProto.java +++ b/src/main/java/build/buf/validate/ValidateProto.java @@ -1316,7 +1316,7 @@ public static void registerAllExtensions( "_in\032Qthis in rules.not_in ? \'value must " + "not be in list %s\'.format([rules.not_in]" + ") : \'\'R\005notInB\013\n\tless_thanB\016\n\014greater_th" + - "anB\010\n\006_const\"\312\027\n\016TimestampRules\022\225\001\n\005cons" + + "anB\010\n\006_const\"\340\027\n\016TimestampRules\022\225\001\n\005cons" + "t\030\002 \001(\0132\032.google.protobuf.TimestampB^\302H[" + "\nY\n\017timestamp.const\032Fthis != rules.const" + " ? \'value must equal %s\'.format([rules.c" + @@ -1390,21 +1390,22 @@ public static void registerAllExtensions( "imestamp.within\032qthis < now-rules.within" + " || this > now+rules.within ? \'value mus" + "t be within %s of now\'.format([rules.wit" + - "hin]) : \'\'H\003R\006within\210\001\001B\013\n\tless_thanB\016\n\014" + - "greater_thanB\010\n\006_constB\t\n\007_within*n\n\nKno" + - "wnRegex\022\033\n\027KNOWN_REGEX_UNSPECIFIED\020\000\022 \n\034" + - "KNOWN_REGEX_HTTP_HEADER_NAME\020\001\022!\n\035KNOWN_", - "REGEX_HTTP_HEADER_VALUE\020\002:_\n\007message\022\037.g" + - "oogle.protobuf.MessageOptions\030\207\t \001(\0132 .b" + - "uf.validate.MessageConstraintsR\007message\210" + - "\001\001:W\n\005oneof\022\035.google.protobuf.OneofOptio" + - "ns\030\207\t \001(\0132\036.buf.validate.OneofConstraint" + - "sR\005oneof\210\001\001:W\n\005field\022\035.google.protobuf.F" + - "ieldOptions\030\207\t \001(\0132\036.buf.validate.FieldC" + - "onstraintsR\005field\210\001\001Bn\n\022build.buf.valida" + - "teB\rValidateProtoP\001ZGbuf.build/gen/go/bu" + - "fbuild/protovalidate/protocolbuffers/go/" + - "buf/validateb\006proto3" + "hin]) : \'\'H\003R\006within\210\001\001\022\024\n\005valid\030\n \001(\010R\005" + + "validB\013\n\tless_thanB\016\n\014greater_thanB\010\n\006_c" + + "onstB\t\n\007_within*n\n\nKnownRegex\022\033\n\027KNOWN_R" + + "EGEX_UNSPECIFIED\020\000\022 \n\034KNOWN_REGEX_HTTP_H", + "EADER_NAME\020\001\022!\n\035KNOWN_REGEX_HTTP_HEADER_" + + "VALUE\020\002:_\n\007message\022\037.google.protobuf.Mes" + + "sageOptions\030\207\t \001(\0132 .buf.validate.Messag" + + "eConstraintsR\007message\210\001\001:W\n\005oneof\022\035.goog" + + "le.protobuf.OneofOptions\030\207\t \001(\0132\036.buf.va" + + "lidate.OneofConstraintsR\005oneof\210\001\001:W\n\005fie" + + "ld\022\035.google.protobuf.FieldOptions\030\207\t \001(\013" + + "2\036.buf.validate.FieldConstraintsR\005field\210" + + "\001\001Bn\n\022build.buf.validateB\rValidateProtoP" + + "\001ZGbuf.build/gen/go/bufbuild/protovalida" + + "te/protocolbuffers/go/buf/validateb\006prot" + + "o3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor .internalBuildGeneratedFileFrom(descriptorData, @@ -1558,7 +1559,7 @@ public static void registerAllExtensions( internal_static_buf_validate_TimestampRules_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( internal_static_buf_validate_TimestampRules_descriptor, - new java.lang.String[] { "Const", "Lt", "Lte", "LtNow", "Gt", "Gte", "GtNow", "Within", "LessThan", "GreaterThan", "Const", "Within", }); + new java.lang.String[] { "Const", "Lt", "Lte", "LtNow", "Gt", "Gte", "GtNow", "Within", "Valid", "LessThan", "GreaterThan", "Const", "Within", }); message.internalInit(descriptor.getExtensions().get(0)); oneof.internalInit(descriptor.getExtensions().get(1)); field.internalInit(descriptor.getExtensions().get(2)); diff --git a/src/main/resources/buf/validate/validate.proto b/src/main/resources/buf/validate/validate.proto index 11f97e95..a9f90da2 100644 --- a/src/main/resources/buf/validate/validate.proto +++ b/src/main/resources/buf/validate/validate.proto @@ -3697,4 +3697,9 @@ message TimestampRules { id: "timestamp.within", expression: "this < now-rules.within || this > now+rules.within ? 'value must be within %s of now'.format([rules.within]) : ''" }]; + + // `valid` specifies that this field, of the `google.protobuf.Timestamp` type, must adhere to the documented specification: + // * the `seconds` field must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. + // * the `nanos` field must be from 0 to 999,999,999 inclusive. + bool valid = 10; }