From d4dcad8b6194047ed7fb871d18cec1d0f18af7d6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 28 Mar 2025 14:05:16 -0400 Subject: [PATCH 01/12] Enhance `isEmail` validation (#250) This enhances the current isEmail validation by using a consistent regex that will be used across protovalidate implementations. --- build.gradle.kts | 1 - conformance/expected-failures.yaml | 40 ------------------- gradle/libs.versions.toml | 1 - .../buf/protovalidate/CustomOverload.java | 30 ++++++-------- 4 files changed, 12 insertions(+), 60 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f5b11d68..b53fb0c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -272,7 +272,6 @@ dependencies { implementation(libs.cel.core) implementation(libs.guava) implementation(libs.ipaddress) - implementation(libs.jakarta.mail.api) buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") diff --git a/conformance/expected-failures.yaml b/conformance/expected-failures.yaml index be9c0ac3..32e5f20b 100644 --- a/conformance/expected-failures.yaml +++ b/conformance/expected-failures.yaml @@ -107,46 +107,6 @@ custom_constraints: #ERROR: :1:1: expression of type 'int' cannot be range of a comprehension (must be list, map, or dynamic) # | this.all(e, e == 1) # | ^ -library/is_email: - - invalid/non_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"µ@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo..bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - valid/empty_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:".@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - - valid/exhaust_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'*+-/=?^_`{|}~@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - - valid/label_all_digits - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@0.1.2.3.4.5.6.7.8.9"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - - valid/multiple_empty_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"...@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" library/is_host_and_port: - port_required/false/invalid/ipv6_zone-id_too_short # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[::1%]"} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e1a9454..382e507b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,6 @@ cel-core = { module = "org.projectnessie.cel:cel-core" } errorprone = { module = "com.google.errorprone:error_prone_core", version = "2.37.0" } guava = { module = "com.google.guava:guava", version = "33.4.0-jre" } ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = "ipaddress" } -jakarta-mail-api = { module = "jakarta.mail:jakarta.mail-api", version = "2.1.3" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.4" } diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index cd60e93f..236df97c 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -20,8 +20,6 @@ import com.google.common.primitives.Bytes; import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddressString; -import jakarta.mail.internet.AddressException; -import jakarta.mail.internet.InternetAddress; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -29,6 +27,7 @@ import java.net.URISyntaxException; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import org.projectnessie.cel.common.types.BoolT; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; @@ -58,6 +57,11 @@ final class CustomOverload { private static final String OVERLOAD_IS_INF = "isInf"; private static final String OVERLOAD_IS_HOST_AND_PORT = "isHostAndPort"; + // See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + private static final Pattern EMAIL_REGEX = + Pattern.compile( + "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"); + /** * Create custom function overload list. * @@ -514,27 +518,17 @@ private static Val uniqueList(Lister list) { } /** - * Validates if the input string is a valid email address. + * validateEmail returns true if addr is a valid email address. + * + *

This regex conforms to the definition for a valid email address from the HTML standard. Note + * that this standard willfully deviates from RFC 5322, which allows many unexpected forms of + * email addresses and will easily match a typographical error. * * @param addr The input string to validate as an email address. * @return {@code true} if the input string is a valid email address, {@code false} otherwise. */ private static boolean validateEmail(String addr) { - try { - InternetAddress emailAddr = new InternetAddress(addr); - emailAddr.validate(); - if (addr.contains("<") || !emailAddr.getAddress().equals(addr)) { - return false; - } - addr = emailAddr.getAddress(); - if (addr.length() > 254) { - return false; - } - String[] parts = addr.split("@", 2); - return parts[0].length() < 64 && validateHostname(parts[1]); - } catch (AddressException ex) { - return false; - } + return EMAIL_REGEX.matcher(addr).matches(); } /** From 5a6c6812a306c26e285b30b822c9f8390624c3ee Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 3 Apr 2025 15:31:11 -0400 Subject: [PATCH 02/12] Enhance IP validation (#256) This beefs up the validation for the following: IP address (v4 and v6) IP prefix (v4 and v6) Hostname / Port --- build.gradle.kts | 1 - conformance/expected-failures.yaml | 88 ----- gradle/libs.versions.toml | 2 - .../buf/protovalidate/CustomOverload.java | 294 ++++++++------- .../java/build/buf/protovalidate/Ipv4.java | 204 +++++++++++ .../java/build/buf/protovalidate/Ipv6.java | 345 ++++++++++++++++++ 6 files changed, 705 insertions(+), 229 deletions(-) create mode 100644 src/main/java/build/buf/protovalidate/Ipv4.java create mode 100644 src/main/java/build/buf/protovalidate/Ipv6.java diff --git a/build.gradle.kts b/build.gradle.kts index b53fb0c7..2bc2fa2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -271,7 +271,6 @@ dependencies { implementation(enforcedPlatform(libs.cel)) implementation(libs.cel.core) implementation(libs.guava) - implementation(libs.ipaddress) buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") diff --git a/conformance/expected-failures.yaml b/conformance/expected-failures.yaml index 32e5f20b..5d92bdca 100644 --- a/conformance/expected-failures.yaml +++ b/conformance/expected-failures.yaml @@ -107,94 +107,6 @@ custom_constraints: #ERROR: :1:1: expression of type 'int' cannot be range of a comprehension (must be list, map, or dynamic) # | this.all(e, e == 1) # | ^ -library/is_host_and_port: - - port_required/false/invalid/ipv6_zone-id_too_short - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[::1%]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # got: valid - - port_required/false/invalid/port_number_sign - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # got: valid - - port_required/false/valid/ipv6_embedded_ipv4 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[0:0:0:0:0:ffff:192.1.56.10]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # message: "" - - port_required/false/valid/ipv6_with_zone-id - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[::1%foo]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # message: "" - - port_required/false/valid/ipv6_zone-id_any_non_null_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[::1%% :x\x1f]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # message: "" - - port_required/true/invalid/port_number_sign - # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0" port_required:true} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" - # got: valid -library/is_ip: - - version/omitted/invalid/ipv6_zone-id - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1%"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" - # got: valid - - version/omitted/valid/ipv6_zone-id - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1%foo"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_ip" - # message: "" - - version/omitted/valid/ipv6_zone-id_any_non_null_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1%% :x\x1f"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_ip" - # message: "" -library/is_ip_prefix: - - version/omitted/strict/omitted/invalid/ipv4_bad_leading_zero_in_prefix-length - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"192.168.1.0/024"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv4_prefix_leading_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:" 127.0.0.1/16"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv4_prefix_trailing_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"127.0.0.1/16 "} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv6_bad_leading_zero_in_prefix-length - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF/024"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv6_prefix_leading_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:" ::1/64"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv6_prefix_trailing_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"::1/64 "} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid - - version/omitted/strict/omitted/invalid/ipv6_zone-id/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"::1%en1/64"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid library/is_uri: - invalid/host/c # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo@你好.com"} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 382e507b..fe4bac59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,6 @@ assertj = "3.27.3" buf = "1.50.1" cel = "0.5.1" -ipaddress = "5.5.1" junit = "5.12.1" maven-publish = "0.31.0" # When updating, make sure to update versions in the following files to match and regenerate code with 'make generate'. @@ -19,7 +18,6 @@ cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" } cel-core = { module = "org.projectnessie.cel:cel-core" } errorprone = { module = "com.google.errorprone:error_prone_core", version = "2.37.0" } guava = { module = "com.google.guava:guava", version = "33.4.0-jre" } -ipaddress = { module = "com.github.seancfoley:ipaddress", version.ref = "ipaddress" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.4" } diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index 236df97c..889b0f78 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -14,18 +14,11 @@ package build.buf.protovalidate; -import com.google.common.base.Ascii; -import com.google.common.base.Splitter; -import com.google.common.net.InetAddresses; import com.google.common.primitives.Bytes; -import inet.ipaddr.IPAddress; -import inet.ipaddr.IPAddressString; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; import org.projectnessie.cel.common.types.BoolT; @@ -74,15 +67,15 @@ static Overload[] create() { startsWith(), endsWith(), contains(), - isHostname(), - isEmail(), - isIp(), - isIpPrefix(), - isUri(), - isUriRef(), + celIsHostname(), + celIsEmail(), + celIsIp(), + celIsIpPrefix(), + celIsUri(), + celIsUriRef(), isNan(), isInf(), - isHostAndPort(), + celIsHostAndPort(), }; } @@ -226,7 +219,7 @@ private static Overload contains() { * * @return The {@link Overload} instance for the "isHostname" operation. */ - private static Overload isHostname() { + private static Overload celIsHostname() { return Overload.unary( OVERLOAD_IS_HOSTNAME, value -> { @@ -234,10 +227,7 @@ private static Overload isHostname() { return Err.noSuchOverload(value, OVERLOAD_IS_HOSTNAME, null); } String host = (String) value.value(); - if (host.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateHostname(host)); + return Types.boolOf(isHostname(host)); }); } @@ -246,7 +236,7 @@ private static Overload isHostname() { * * @return The {@link Overload} instance for the "isEmail" operation. */ - private static Overload isEmail() { + private static Overload celIsEmail() { return Overload.unary( OVERLOAD_IS_EMAIL, value -> { @@ -254,10 +244,7 @@ private static Overload isEmail() { return Err.noSuchOverload(value, OVERLOAD_IS_EMAIL, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateEmail(addr)); + return Types.boolOf(isEmail(addr)); }); } @@ -266,7 +253,7 @@ private static Overload isEmail() { * * @return The {@link Overload} instance for the "isIp" operation. */ - private static Overload isIp() { + private static Overload celIsIp() { return Overload.overload( OVERLOAD_IS_IP, null, @@ -275,20 +262,14 @@ private static Overload isIp() { return Err.noSuchOverload(value, OVERLOAD_IS_IP, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateIP(addr, 0L)); + return Types.boolOf(isIP(addr, 0L)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String || rhs.type().typeEnum() != TypeEnum.Int) { return Err.noSuchOverload(lhs, OVERLOAD_IS_IP, rhs); } String address = (String) lhs.value(); - if (address.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateIP(address, rhs.intValue())); + return Types.boolOf(isIP(address, rhs.intValue())); }, null); } @@ -298,7 +279,7 @@ private static Overload isIp() { * * @return The {@link Overload} instance for the "isIpPrefix" operation. */ - private static Overload isIpPrefix() { + private static Overload celIsIpPrefix() { return Overload.overload( OVERLOAD_IS_IP_PREFIX, null, @@ -308,10 +289,7 @@ private static Overload isIpPrefix() { return Err.noSuchOverload(value, OVERLOAD_IS_IP_PREFIX, null); } String prefix = (String) value.value(); - if (prefix.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateIPPrefix(prefix, 0L, false)); + return Types.boolOf(isIPPrefix(prefix, 0L, false)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String @@ -320,13 +298,10 @@ private static Overload isIpPrefix() { return Err.noSuchOverload(lhs, OVERLOAD_IS_IP_PREFIX, rhs); } String prefix = (String) lhs.value(); - if (prefix.isEmpty()) { - return BoolT.False; - } if (rhs.type().typeEnum() == TypeEnum.Int) { - return Types.boolOf(validateIPPrefix(prefix, rhs.intValue(), false)); + return Types.boolOf(isIPPrefix(prefix, rhs.intValue(), false)); } - return Types.boolOf(validateIPPrefix(prefix, 0L, rhs.booleanValue())); + return Types.boolOf(isIPPrefix(prefix, 0L, rhs.booleanValue())); }, (values) -> { if (values.length != 3 @@ -336,11 +311,7 @@ private static Overload isIpPrefix() { return Err.noSuchOverload(values[0], OVERLOAD_IS_IP_PREFIX, "", values); } String prefix = (String) values[0].value(); - if (prefix.isEmpty()) { - return BoolT.False; - } - return Types.boolOf( - validateIPPrefix(prefix, values[1].intValue(), values[2].booleanValue())); + return Types.boolOf(isIPPrefix(prefix, values[1].intValue(), values[2].booleanValue())); }); } @@ -349,7 +320,7 @@ private static Overload isIpPrefix() { * * @return The {@link Overload} instance for the "isUri" operation. */ - private static Overload isUri() { + private static Overload celIsUri() { return Overload.unary( OVERLOAD_IS_URI, value -> { @@ -369,7 +340,7 @@ private static Overload isUri() { * * @return The {@link Overload} instance for the "isUriRef" operation. */ - private static Overload isUriRef() { + private static Overload celIsUriRef() { return Overload.unary( OVERLOAD_IS_URI_REF, value -> { @@ -432,7 +403,7 @@ private static Overload isInf() { null); } - private static Overload isHostAndPort() { + private static Overload celIsHostAndPort() { return Overload.overload( OVERLOAD_IS_HOST_AND_PORT, null, @@ -443,39 +414,74 @@ private static Overload isHostAndPort() { } String value = (String) lhs.value(); boolean portRequired = rhs.booleanValue(); - return Types.boolOf(hostAndPort(value, portRequired)); + return Types.boolOf(isHostAndPort(value, portRequired)); }, null); } - private static boolean hostAndPort(String value, boolean portRequired) { - if (value.isEmpty()) { + /** + * Returns true if the string is a valid host/port pair, for example "example.com:8080". + * + *

If the argument portRequired is true, the port is required. If the argument is false, the + * port is optional. + * + *

The host can be one of: + * + *

+ * + *

The port is separated by a colon. It must be non-empty, with a decimal number in the range + * of 0-65535, inclusive. + */ + private static boolean isHostAndPort(String str, boolean portRequired) { + if (str.isEmpty()) { return false; } - int splitIdx = value.lastIndexOf(':'); - if (value.charAt(0) == '[') { // ipv6 - int end = value.indexOf(']'); - if (end + 1 == value.length()) { // no port - return !portRequired && validateIP(value.substring(1, end), 6); - } - if (end + 1 == splitIdx) { // port - return validateIP(value.substring(1, end), 6) - && validatePort(value.substring(splitIdx + 1)); + + int splitIdx = str.lastIndexOf(':'); + + if (str.charAt(0) == '[') { + int end = str.lastIndexOf(']'); + + int endPlus = end + 1; + if (endPlus == str.length()) { // no port + return !portRequired && isIP(str.substring(1, end), 6); + } else if (endPlus == splitIdx) { // port + return isIP(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); } return false; // malformed } + if (splitIdx < 0) { - return !portRequired && (validateHostname(value) || validateIP(value, 4)); + return !portRequired && (isHostname(str) || isIP(str, 4)); } - String host = value.substring(0, splitIdx); - String port = value.substring(splitIdx + 1); - return (validateHostname(host) || validateIP(host, 4)) && validatePort(port); + + String host = str.substring(0, splitIdx); + String port = str.substring(splitIdx + 1); + + return ((isHostname(host) || isIP(host, 4)) && isPort(port)); } - private static boolean validatePort(String value) { + // Returns true if the string is a valid port for isHostAndPort. + private static boolean isPort(String str) { + if (str.isEmpty()) { + return false; + } + + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if ('0' <= c && c <= '9') { + continue; + } + return false; + } + try { - int portNum = Integer.parseInt(value); - return portNum >= 0 && portNum <= 65535; + int val = Integer.parseInt(str); + return val <= 65535; } catch (NumberFormatException nfe) { return false; } @@ -518,7 +524,7 @@ private static Val uniqueList(Lister list) { } /** - * validateEmail returns true if addr is a valid email address. + * isEmail returns true if addr is a valid email address. * *

This regex conforms to the definition for a valid email address from the HTML standard. Note * that this standard willfully deviates from RFC 5322, which allows many unexpected forms of @@ -527,66 +533,86 @@ private static Val uniqueList(Lister list) { * @param addr The input string to validate as an email address. * @return {@code true} if the input string is a valid email address, {@code false} otherwise. */ - private static boolean validateEmail(String addr) { + private static boolean isEmail(String addr) { return EMAIL_REGEX.matcher(addr).matches(); } /** - * Validates if the input string is a valid hostname. + * Returns true if the string is a valid hostname, for example "foo.example.com". * - * @param host The input string to validate as a hostname. - * @return {@code true} if the input string is a valid hostname, {@code false} otherwise. + *

A valid hostname follows the rules below: + * + *

*/ - private static boolean validateHostname(String host) { - if (host.length() > 253) { + private static boolean isHostname(String val) { + if (val.length() > 253) { return false; } - String s = Ascii.toLowerCase(host.endsWith(".") ? host.substring(0, host.length() - 1) : host); - Iterable parts = Splitter.on('.').split(s); + + String str; + if (val.endsWith(".")) { + str = val.substring(0, val.length() - 1); + } else { + str = val; + } + boolean allDigits = false; + + String[] parts = str.toLowerCase(Locale.getDefault()).split("\\.", -1); + + // split hostname on '.' and validate each part for (String part : parts) { allDigits = true; - int l = part.length(); - if (l == 0 || l > 63 || part.charAt(0) == '-' || part.charAt(l - 1) == '-') { + + // if part is empty, longer than 63 chars, or starts/ends with '-', it is invalid + int len = part.length(); + if (len == 0 || len > 63 || part.startsWith("-") || part.endsWith("-")) { return false; } - for (int i = 0; i < l; i++) { - char ch = part.charAt(i); - if (!Ascii.isLowerCase(ch) && !isDigit(ch) && ch != '-') { + + // for each character in part + for (int i = 0; i < part.length(); i++) { + char c = part.charAt(i); + // if the character is not a-z, 0-9, or '-', it is invalid + if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-') { return false; } - allDigits = allDigits && isDigit(ch); + + allDigits = allDigits && c >= '0' && c <= '9'; } } + // the last part cannot be all numbers return !allDigits; } - private static boolean isDigit(char c) { - return c >= '0' && c <= '9'; - } - /** - * Validates if the input string is a valid IP address. + * Returns true if the string is an IPv4 or IPv6 address, optionally limited to a specific + * version. + * + *

Version 0 means either 4 or 6. Passing a version other than 0, 4, or 6 always returns false. + * + *

IPv4 addresses are expected in the dotted decimal format, for example "192.168.5.21". IPv6 + * addresses are expected in their text representation, for example "::1", or + * "2001:0DB8:ABCD:0012::0". * - * @param addr The input string to validate as an IP address. - * @param ver The IP version to validate against (0 for any version, 4 for IPv4, 6 for IPv6). - * @return {@code true} if the input string is a valid IP address of the specified version, {@code - * false} otherwise. + *

Both formats are well-defined in the internet standard RFC 3986. Zone identifiers for IPv6 + * addresses (for example "fe80::a%en1") are supported. */ - private static boolean validateIP(String addr, long ver) { - InetAddress address; - try { - address = InetAddresses.forString(addr); - } catch (Exception e) { - return false; - } - if (ver == 0L) { - return true; + private static boolean isIP(String addr, long ver) { + if (ver == 6L) { + return new Ipv6(addr).address(); } else if (ver == 4L) { - return address instanceof Inet4Address; - } else if (ver == 6L) { - return address instanceof Inet6Address; + return new Ipv4(addr).address(); + } else if (ver == 0L) { + return new Ipv4(addr).address() || new Ipv6(addr).address(); } return false; } @@ -611,39 +637,31 @@ private static boolean validateURI(String val, boolean checkAbsolute) { } /** - * Validates if the input string is a valid IP prefix. + * Returns true if the string is a valid IP with prefix length, optionally limited to a specific + * version (v4 or v6), and optionally requiring the host portion to be all zeros. * - * @param prefix The input string to validate as an IP prefix. - * @param ver The IP version to validate against (0 for any version, 4 for IPv4, 6 for IPv6). - * @param strict If strict is true and host bits are set in the supplied address, then false is - * returned. - * @return {@code true} if the input string is a valid IP prefix of the specified version, {@code - * false} otherwise. + *

An address prefix divides an IP address into a network portion, and a host portion. The + * prefix length specifies how many bits the network portion has. For example, the IPv6 prefix + * "2001:db8:abcd:0012::0/64" designates the left-most 64 bits as the network prefix. The range of + * the network is 2**64 addresses, from 2001:db8:abcd:0012::0 to + * 2001:db8:abcd:0012:ffff:ffff:ffff:ffff. + * + *

An address prefix may include a specific host address, for example + * "2001:db8:abcd:0012::1f/64". With strict = true, this is not permitted. The host portion must + * be all zeros, as in "2001:db8:abcd:0012::0/64". + * + *

The same principle applies to IPv4 addresses. "192.168.1.0/24" designates the first 24 bits + * of the 32-bit IPv4 as the network prefix. */ - private static boolean validateIPPrefix(String prefix, long ver, boolean strict) { - IPAddressString str; - IPAddress addr; - try { - str = new IPAddressString(prefix); - addr = str.toAddress(); - } catch (Exception e) { - return false; - } - if (!addr.isPrefixed()) { - return false; - } - if (strict) { - IPAddress mask = addr.getNetworkMask().withoutPrefixLength(); - if (!addr.mask(mask).equals(str.getHostAddress())) { - return false; - } - } - if (ver == 0L) { - return true; - } else if (ver == 4L) { - return addr.isIPv4(); - } else if (ver == 6L) { - return addr.isIPv6(); + private static boolean isIPPrefix(String str, long version, boolean strict) { + if (version == 6L) { + Ipv6 ip = new Ipv6(str); + return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); + } else if (version == 4L) { + Ipv4 ip = new Ipv4(str); + return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); + } else if (version == 0L) { + return isIPPrefix(str, 6, strict) || isIPPrefix(str, 4, strict); } return false; } diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java new file mode 100644 index 00000000..52f249a3 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -0,0 +1,204 @@ +// Copyright 2023-2024 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; + +import java.util.ArrayList; +import java.util.List; + +final class Ipv4 { + private String str; + private int index; + private List octets; + private int prefixLen; + + Ipv4(String str) { + this.str = str; + this.octets = new ArrayList(); + } + + /** + * Returns the 32-bit value of an address parsed through address() or addressPrefix(). + * + *

Returns -1 if no address was parsed successfully. + */ + int getBits() { + if (this.octets.size() != 4) { + return -1; + } + return (this.octets.get(0) << 24) + | (this.octets.get(1) << 16) + | (this.octets.get(2) << 8) + | this.octets.get(3); + } + + /** + * Returns true if all bits to the right of the prefix-length are all zeros. + * + *

Behavior is undefined if addressPrefix() has not been called before, or has returned false. + */ + boolean isPrefixOnly() { + int bits = this.getBits(); + + int mask = 0; + if (this.prefixLen == 32) { + mask = 0xffffffff; + } else { + mask = ~(0xffffffff >>> this.prefixLen); + } + + int masked = bits & mask; + + return bits == masked; + } + + // Parses an IPv4 Address in dotted decimal notation. + boolean address() { + return this.addressPart() && this.index == this.str.length(); + } + + // Parses an IPv4 Address prefix. + boolean addressPrefix() { + return this.addressPart() + && this.take('/') + && this.prefixLength() + && this.index == this.str.length(); + } + + // Stores value in `prefixLen` + private boolean prefixLength() { + int start = this.index; + + while (this.index < this.str.length() && this.digit()) { + if (this.index - start > 2) { + // max prefix-length is 32 bits, so anything more than 2 digits is invalid + return false; + } + } + + String str = this.str.substring(start, this.index); + if (str.isEmpty()) { + // too short + return false; + } + + if (str.length() > 1 && str.charAt(0) == '0') { + // bad leading 0 + return false; + } + + try { + int val = Integer.parseInt(str); + + if (val > 32) { + // max 32 bits + return false; + } + + this.prefixLen = val; + return true; + } catch (NumberFormatException nfe) { + return false; + } + } + + private boolean addressPart() { + int start = this.index; + + if (this.decOctet() + && this.take('.') + && this.decOctet() + && this.take('.') + && this.decOctet() + && this.take('.') + && this.decOctet()) { + return true; + } + + this.index = start; + + return false; + } + + private boolean decOctet() { + int start = this.index; + + while (this.index < this.str.length() && this.digit()) { + if (this.index - start > 3) { + // decimal octet can be three characters at most + return false; + } + } + + String str = this.str.substring(start, this.index); + if (str.isEmpty()) { + // too short + return false; + } + + if (str.length() > 1 && str.charAt(0) == '0') { + // bad leading 0 + return false; + } + + try { + int val = Integer.parseInt(str); + + if (val > 255) { + return false; + } + + this.octets.add((short) val); + + return true; + } catch (NumberFormatException nfe) { + // Error converting to number + return false; + } + } + + /** + * Reports whether the current position is a digit. + * + *

Method parses the rule: + * + *

DIGIT = %x30-39 ; 0-9
+   */
+  private boolean digit() {
+    char c = this.str.charAt(this.index);
+    if ('0' <= c && c <= '9') {
+      this.index++;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Take the given char at the current index.
+   *
+   * 

If char is at the current index, increment the index. + */ + private boolean take(char c) { + if (this.index >= this.str.length()) { + return false; + } + + if (this.str.charAt(this.index) == c) { + this.index++; + return true; + } + + return false; + } +} diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java new file mode 100644 index 00000000..aa178943 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Ipv6.java @@ -0,0 +1,345 @@ +// Copyright 2023-2024 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; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +final class Ipv6 { + private String str; + private int index; + // 16-bit pieces found + private List pieces; + // number of 16-bit pieces found when double colon was found + private int doubleColonAt; + private boolean doubleColonSeen; + // dotted notation for right-most 32 bits + private String dottedRaw; + // dotted notation successfully parsed as IPv4 + @Nullable private Ipv4 dottedAddr; + private boolean zoneIDFound; + // 0 - 128 + private int prefixLen; + + Ipv6(String str) { + this.str = str; + this.pieces = new ArrayList(); + this.doubleColonAt = -1; + this.dottedRaw = ""; + } + + /** + * Returns the 128-bit value of an address parsed through address() or addressPrefix() as a + * 2-element length array of 64-bit values. + * + *

Returns [0L, 0L] if no address was parsed successfully. + */ + private long[] getBits() { + List p16 = this.pieces; + + // handle dotted decimal, add to p16 + if (this.dottedAddr != null) { + // right-most 32 bits + long dotted32 = this.dottedAddr.getBits(); + // high 16 bits + p16.add((int) (dotted32 >> 16)); + // low 16 bits + p16.add((int) dotted32); + } + + // handle double colon, fill pieces with 0 + if (this.doubleColonSeen) { + while (p16.size() < 8) { + p16.add(this.doubleColonAt, 0x00000000); + } + } + + if (p16.size() != 8) { + return new long[] {0L, 0L}; + } + + return new long[] { + Long.valueOf(p16.get(0)) << 48 + | Long.valueOf(p16.get(1)) << 32 + | Long.valueOf(p16.get(2)) << 16 + | Long.valueOf(p16.get(3)), + Long.valueOf(p16.get(4)) << 48 + | Long.valueOf(p16.get(5)) << 32 + | Long.valueOf(p16.get(6)) << 16 + | Long.valueOf(p16.get(7)) + }; + } + + boolean isPrefixOnly() { + // For each 64-bit piece of the address, require that values to the right of the prefix are zero + long[] bits = this.getBits(); + for (int i = 0; i < bits.length; i++) { + long p64 = bits[i]; + long size = this.prefixLen - 64L * i; + + long mask = 0L; + if (size >= 64) { + mask = 0xFFFFFFFFFFFFFFFFL; + } else if (size < 0) { + mask = 0x0; + } else { + mask = ~(0xFFFFFFFFFFFFFFFFL >>> size); + } + long masked = p64 & mask; + if (p64 != masked) { + return false; + } + } + + return true; + } + + // Parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. + boolean address() { + return this.addressPart() && this.index == this.str.length(); + } + + // Parse IPv6 Address Prefix following RFC 4291. Zone id is not permitted. + boolean addressPrefix() { + return this.addressPart() + && !this.zoneIDFound + && this.take('/') + && this.prefixLength() + && this.index == this.str.length(); + } + + // Stores value in `prefixLen` + private boolean prefixLength() { + int start = this.index; + + while (this.index < this.str.length() && this.digit()) { + if (this.index - start > 3) { + return false; + } + } + + String str = this.str.substring(start, this.index); + + if (str.isEmpty()) { + // too short + return false; + } + + if (str.length() > 1 && str.charAt(0) == '0') { + // bad leading 0 + return false; + } + + try { + int val = Integer.parseInt(str); + + if (val > 128) { + // max 128 bits + return false; + } + + this.prefixLen = val; + return true; + } catch (NumberFormatException nfe) { + // Error converting to number + return false; + } + } + + // Stores dotted notation for right-most 32 bits in `dottedRaw` / `dottedAddr` if found. + private boolean addressPart() { + while (this.index < this.str.length()) { + // dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10 + if ((this.doubleColonSeen || this.pieces.size() == 6) && this.dotted()) { + Ipv4 dotted = new Ipv4(this.dottedRaw); + if (dotted.address()) { + this.dottedAddr = dotted; + return true; + } + return false; + } + + if (this.h16()) { + continue; + } + + if (this.take(':')) { + if (this.take(':')) { + if (this.doubleColonSeen) { + return false; + } + + this.doubleColonSeen = true; + this.doubleColonAt = this.pieces.size(); + if (this.take(':')) { + return false; + } + } + continue; + } + + if (this.str.charAt(this.index) == '%' && !this.zoneID()) { + return false; + } + + break; + } + + return this.doubleColonSeen || this.pieces.size() == 8; + } + + /** + * There is no definition for the character set allowed in the zone identifier. RFC 4007 permits + * basically any non-null string. + * + *

RFC 6874: ZoneID = 1*( unreserved / pct-encoded )
+   */
+  private boolean zoneID() {
+    int start = this.index;
+
+    if (this.take('%')) {
+      if (this.str.length() - this.index > 0) {
+        // permit any non-null string
+        this.index = this.str.length();
+        this.zoneIDFound = true;
+
+        return true;
+      }
+    }
+
+    this.index = start;
+    this.zoneIDFound = false;
+
+    return false;
+  }
+
+  /**
+   * Determines whether string contains a dotted address.
+   *
+   * 

Method parses the rule: + * + *

1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+   *
+   * 

Stores match in dottedRaw. + */ + private boolean dotted() { + int start = this.index; + + this.dottedRaw = ""; + + while (this.index < this.str.length() && (this.digit() || this.take('.'))) {} + + if (this.index - start >= 7) { + this.dottedRaw = this.str.substring(start, this.index); + + return true; + } + + this.index = start; + + return false; + } + + /** + * Determine whether string contains an h16. + * + *

Method parses the rule: + * + *

h16 = 1*4HEXDIG
+   *
+   * 

Stores 16-bit value in pieces. + */ + private boolean h16() { + int start = this.index; + + while (this.index < this.str.length() && this.hexDig()) {} + + String str = this.str.substring(start, this.index); + + if (str.isEmpty()) { + // too short + return false; + } + + if (str.length() > 4) { + // too long + return false; + } + + try { + int val = Integer.parseInt(str, 16); + + this.pieces.add(val); + return true; + } catch (NumberFormatException nfe) { + // Error converting to number + return false; + } + } + + /** + * Reports whether the current position is a hex digit. + * + *

Method parses the rule: + * + *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+   */
+  private boolean hexDig() {
+    char c = this.str.charAt(this.index);
+
+    if (('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')) {
+      this.index++;
+
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Reports whether the current position is a digit.
+   *
+   * 

Method parses the rule: + * + *

DIGIT = %x30-39 ; 0-9
+   */
+  private boolean digit() {
+    char c = this.str.charAt(this.index);
+    if ('0' <= c && c <= '9') {
+      this.index++;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Take the given char at the current index.
+   *
+   * 

If char is at the current index, increment the index. + */ + private boolean take(char c) { + if (this.index >= this.str.length()) { + return false; + } + + if (this.str.charAt(this.index) == c) { + this.index++; + return true; + } + + return false; + } +} From f98b4b9f7a85bdcf843800b63c2b8b35f1eefca5 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 9 Apr 2025 10:34:01 -0400 Subject: [PATCH 03/12] Enhance URI validation (#257) This ports the validation logic from protovalidate-go for validating URIs and URI references. --- conformance/expected-failures.yaml | 90 -- .../buf/protovalidate/CustomOverload.java | 96 +- .../java/build/buf/protovalidate/Ipv4.java | 13 +- .../java/build/buf/protovalidate/Ipv6.java | 27 +- .../java/build/buf/protovalidate/Uri.java | 949 ++++++++++++++++++ 5 files changed, 1015 insertions(+), 160 deletions(-) create mode 100644 src/main/java/build/buf/protovalidate/Uri.java diff --git a/conformance/expected-failures.yaml b/conformance/expected-failures.yaml index 5d92bdca..0db100ef 100644 --- a/conformance/expected-failures.yaml +++ b/conformance/expected-failures.yaml @@ -107,96 +107,6 @@ custom_constraints: #ERROR: :1:1: expression of type 'int' cannot be range of a comprehension (must be list, map, or dynamic) # | this.all(e, e == 1) # | ^ -library/is_uri: - - invalid/host/c - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo@你好.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://2001:0db8:85a3:0000:0000:8a2e:0370:7334"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6_zone-id_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6_zone-id_unquoted - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%eth0]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_reg-name_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo%c3x%96"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/port/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:8a"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/port/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/userinfo_reserved_at - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://@@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - valid/host_ipfuture_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[vF.-!$&'()*+,;=._~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipfuture_long - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1234AF.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipfuture_short - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipv6_zone-id_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%61%20%23]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipv6_zone-id_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%c3%96]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/path-empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" -library/is_uri_ref: - - valid/empty_string - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - - valid/path-empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" standard_constraints/ignore: - proto/2023/map/ignore_always/invalid/populated # input: [type.googleapis.com/buf.validate.conformance.cases.EditionsMapIgnoreAlways]:{val:{key:1 value:1}} diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index 889b0f78..94cc4819 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -15,8 +15,6 @@ package build.buf.protovalidate; import com.google.common.primitives.Bytes; -import java.net.URI; -import java.net.URISyntaxException; import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -62,19 +60,19 @@ final class CustomOverload { */ static Overload[] create() { return new Overload[] { - format(), - unique(), - startsWith(), - endsWith(), - contains(), + celFormat(), + celUnique(), + celStartsWith(), + celEndsWith(), + celContains(), celIsHostname(), celIsEmail(), celIsIp(), celIsIpPrefix(), celIsUri(), celIsUriRef(), - isNan(), - isInf(), + celIsNan(), + celIsInf(), celIsHostAndPort(), }; } @@ -84,7 +82,7 @@ static Overload[] create() { * * @return The {@link Overload} instance for the "format" operation. */ - private static Overload format() { + private static Overload celFormat() { return Overload.binary( OVERLOAD_FORMAT, (lhs, rhs) -> { @@ -106,7 +104,7 @@ private static Overload format() { * * @return The {@link Overload} instance for the "unique" operation. */ - private static Overload unique() { + private static Overload celUnique() { return Overload.unary( OVERLOAD_UNIQUE, (val) -> { @@ -122,7 +120,7 @@ private static Overload unique() { * * @return The {@link Overload} instance for the "startsWith" operation. */ - private static Overload startsWith() { + private static Overload celStartsWith() { return Overload.binary( OVERLOAD_STARTS_WITH, (lhs, rhs) -> { @@ -157,7 +155,7 @@ private static Overload startsWith() { * * @return The {@link Overload} instance for the "endsWith" operation. */ - private static Overload endsWith() { + private static Overload celEndsWith() { return Overload.binary( OVERLOAD_ENDS_WITH, (lhs, rhs) -> { @@ -192,7 +190,7 @@ private static Overload endsWith() { * * @return The {@link Overload} instance for the "contains" operation. */ - private static Overload contains() { + private static Overload celContains() { return Overload.binary( OVERLOAD_CONTAINS, (lhs, rhs) -> { @@ -262,14 +260,14 @@ private static Overload celIsIp() { return Err.noSuchOverload(value, OVERLOAD_IS_IP, null); } String addr = (String) value.value(); - return Types.boolOf(isIP(addr, 0L)); + return Types.boolOf(isIp(addr, 0L)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String || rhs.type().typeEnum() != TypeEnum.Int) { return Err.noSuchOverload(lhs, OVERLOAD_IS_IP, rhs); } String address = (String) lhs.value(); - return Types.boolOf(isIP(address, rhs.intValue())); + return Types.boolOf(isIp(address, rhs.intValue())); }, null); } @@ -289,7 +287,7 @@ private static Overload celIsIpPrefix() { return Err.noSuchOverload(value, OVERLOAD_IS_IP_PREFIX, null); } String prefix = (String) value.value(); - return Types.boolOf(isIPPrefix(prefix, 0L, false)); + return Types.boolOf(isIpPrefix(prefix, 0L, false)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String @@ -299,9 +297,9 @@ private static Overload celIsIpPrefix() { } String prefix = (String) lhs.value(); if (rhs.type().typeEnum() == TypeEnum.Int) { - return Types.boolOf(isIPPrefix(prefix, rhs.intValue(), false)); + return Types.boolOf(isIpPrefix(prefix, rhs.intValue(), false)); } - return Types.boolOf(isIPPrefix(prefix, 0L, rhs.booleanValue())); + return Types.boolOf(isIpPrefix(prefix, 0L, rhs.booleanValue())); }, (values) -> { if (values.length != 3 @@ -311,7 +309,7 @@ private static Overload celIsIpPrefix() { return Err.noSuchOverload(values[0], OVERLOAD_IS_IP_PREFIX, "", values); } String prefix = (String) values[0].value(); - return Types.boolOf(isIPPrefix(prefix, values[1].intValue(), values[2].booleanValue())); + return Types.boolOf(isIpPrefix(prefix, values[1].intValue(), values[2].booleanValue())); }); } @@ -328,10 +326,7 @@ private static Overload celIsUri() { return Err.noSuchOverload(value, OVERLOAD_IS_URI, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateURI(addr, true)); + return Types.boolOf(isUri(addr)); }); } @@ -348,10 +343,7 @@ private static Overload celIsUriRef() { return Err.noSuchOverload(value, OVERLOAD_IS_URI_REF, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(validateURI(addr, false)); + return Types.boolOf(isUriRef(addr)); }); } @@ -360,7 +352,7 @@ private static Overload celIsUriRef() { * * @return The {@link Overload} instance for the "isNan" operation. */ - private static Overload isNan() { + private static Overload celIsNan() { return Overload.unary( OVERLOAD_IS_NAN, value -> { @@ -377,7 +369,7 @@ private static Overload isNan() { * * @return The {@link Overload} instance for the "isInf" operation. */ - private static Overload isInf() { + private static Overload celIsInf() { return Overload.overload( OVERLOAD_IS_INF, null, @@ -448,21 +440,21 @@ private static boolean isHostAndPort(String str, boolean portRequired) { int endPlus = end + 1; if (endPlus == str.length()) { // no port - return !portRequired && isIP(str.substring(1, end), 6); + return !portRequired && isIp(str.substring(1, end), 6); } else if (endPlus == splitIdx) { // port - return isIP(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); + return isIp(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); } return false; // malformed } if (splitIdx < 0) { - return !portRequired && (isHostname(str) || isIP(str, 4)); + return !portRequired && (isHostname(str) || isIp(str, 4)); } String host = str.substring(0, splitIdx); String port = str.substring(splitIdx + 1); - return ((isHostname(host) || isIP(host, 4)) && isPort(port)); + return ((isHostname(host) || isIp(host, 4)) && isPort(port)); } // Returns true if the string is a valid port for isHostAndPort. @@ -606,7 +598,7 @@ private static boolean isHostname(String val) { *

Both formats are well-defined in the internet standard RFC 3986. Zone identifiers for IPv6 * addresses (for example "fe80::a%en1") are supported. */ - private static boolean isIP(String addr, long ver) { + static boolean isIp(String addr, long ver) { if (ver == 6L) { return new Ipv6(addr).address(); } else if (ver == 4L) { @@ -618,22 +610,24 @@ private static boolean isIP(String addr, long ver) { } /** - * Validates if the input string is a valid URI, which can be a URL or a URN. + * Returns true if the string is a URI, for example "https://example.com/foo/bar?baz=quux#frag". * - * @param val The input string to validate as a URI. - * @param checkAbsolute Whether to check if this URI is absolute (i.e. has a scheme component) - * @return {@code true} if the input string is a valid URI, {@code false} otherwise. + *

URI is defined in the internet standard RFC 3986. Zone Identifiers in IPv6 address literals + * are supported (RFC 6874). */ - private static boolean validateURI(String val, boolean checkAbsolute) { - try { - URI uri = new URI(val); - if (checkAbsolute) { - return uri.isAbsolute(); - } - return true; - } catch (URISyntaxException e) { - return false; - } + private static boolean isUri(String str) { + return new Uri(str).uri(); + } + + /** + * Returns true if the string is a URI Reference - a URI such as + * "https://example.com/foo/bar?baz=quux#frag", or a Relative Reference such as "./foo/bar?query". + * + *

URI, URI Reference, and Relative Reference are defined in the internet standard RFC 3986. + * Zone Identifiers in IPv6 address literals are supported (RFC 6874). + */ + private static boolean isUriRef(String str) { + return new Uri(str).uriReference(); } /** @@ -653,7 +647,7 @@ private static boolean validateURI(String val, boolean checkAbsolute) { *

The same principle applies to IPv4 addresses. "192.168.1.0/24" designates the first 24 bits * of the 32-bit IPv4 as the network prefix. */ - private static boolean isIPPrefix(String str, long version, boolean strict) { + private static boolean isIpPrefix(String str, long version, boolean strict) { if (version == 6L) { Ipv6 ip = new Ipv6(str); return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); @@ -661,7 +655,7 @@ private static boolean isIPPrefix(String str, long version, boolean strict) { Ipv4 ip = new Ipv4(str); return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); } else if (version == 0L) { - return isIPPrefix(str, 6, strict) || isIPPrefix(str, 4, strict); + return isIpPrefix(str, 6, strict) || isIpPrefix(str, 4, strict); } return false; } diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java index 52f249a3..c3726b64 100644 --- a/src/main/java/build/buf/protovalidate/Ipv4.java +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -17,6 +17,9 @@ import java.util.ArrayList; import java.util.List; +/** + * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address prefix. + */ final class Ipv4 { private String str; private int index; @@ -76,7 +79,7 @@ boolean addressPrefix() { && this.index == this.str.length(); } - // Stores value in `prefixLen` + // Store value in prefixLen private boolean prefixLength() { int start = this.index; @@ -169,9 +172,9 @@ private boolean decOctet() { } /** - * Reports whether the current position is a digit. + * Determines whether the current position is a digit. * - *

Method parses the rule: + *

Parses the rule: * *

DIGIT = %x30-39 ; 0-9
    */
@@ -185,9 +188,7 @@ private boolean digit() {
   }
 
   /**
-   * Take the given char at the current index.
-   *
-   * 

If char is at the current index, increment the index. + * Take the given char at the current position, incrementing the index if necessary. */ private boolean take(char c) { if (this.index >= this.str.length()) { diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java index aa178943..db14eb68 100644 --- a/src/main/java/build/buf/protovalidate/Ipv6.java +++ b/src/main/java/build/buf/protovalidate/Ipv6.java @@ -18,6 +18,9 @@ import java.util.List; import javax.annotation.Nullable; +/** + * Ipv6 is a class used to parse a given string to determine if it is an IPv6 address or address prefix. + */ final class Ipv6 { private String str; private int index; @@ -121,7 +124,7 @@ boolean addressPrefix() { && this.index == this.str.length(); } - // Stores value in `prefixLen` + // Stores value in prefixLen private boolean prefixLength() { int start = this.index; @@ -159,7 +162,7 @@ private boolean prefixLength() { } } - // Stores dotted notation for right-most 32 bits in `dottedRaw` / `dottedAddr` if found. + // Stores dotted notation for right-most 32 bits in dottedRaw / dottedAddr if found. private boolean addressPart() { while (this.index < this.str.length()) { // dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10 @@ -227,9 +230,9 @@ private boolean zoneID() { } /** - * Determines whether string contains a dotted address. + * Determines whether the current position is a dotted address. * - *

Method parses the rule: + *

Parses the rule: * *

1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
    *
@@ -254,9 +257,9 @@ private boolean dotted() {
   }
 
   /**
-   * Determine whether string contains an h16.
+   * Determines whether the current position is an h16.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

h16 = 1*4HEXDIG
    *
@@ -291,9 +294,9 @@ private boolean h16() {
   }
 
   /**
-   * Reports whether the current position is a hex digit.
+   * Determines whether the current position is a hex digit.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
    */
@@ -310,9 +313,9 @@ private boolean hexDig() {
   }
 
   /**
-   * Reports whether the current position is a digit.
+   * Determines whether the current position is a digit.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

DIGIT = %x30-39 ; 0-9
    */
@@ -326,9 +329,7 @@ private boolean digit() {
   }
 
   /**
-   * Take the given char at the current index.
-   *
-   * 

If char is at the current index, increment the index. + * Take the given char at the current position, incrementing the index if necessary. */ private boolean take(char c) { if (this.index >= this.str.length()) { diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java new file mode 100644 index 00000000..63287135 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -0,0 +1,949 @@ +// Copyright 2023-2024 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; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; + +/** + * Ipv6 is a class used to parse a given string to determine if it is a URI or URI reference. + */ +final class Uri { + private String str; + private int index; + private boolean pctEncodedFound; + + Uri(String str) { + this.str = str; + } + + /** + * Determines whether string is a valid URI. + * + *

Parses the rule: + * + *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+   */
+  boolean uri() {
+    int start = this.index;
+
+    if (!(this.scheme() && this.take(':') && this.hierPart())) {
+      this.index = start;
+      return false;
+    }
+
+    if (this.take('?') && !this.query()) {
+      return false;
+    }
+
+    if (this.take('#') && !this.fragment()) {
+      return false;
+    }
+
+    if (this.index != this.str.length()) {
+      this.index = start;
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Determines whether the current position is a valid hier-part.
+   *
+   * 

Parses the rule: + * + *

hier-part = "//" authority path-abempty
+   *                / path-absolute
+   *                / path-rootless
+   *                / path-empty
+   */
+  private boolean hierPart() {
+    int start = this.index;
+
+    if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) {
+      return true;
+    }
+
+    this.index = start;
+
+    return this.pathAbsolute() || this.pathRootless() || this.pathEmpty();
+  }
+
+  /**
+   * Determines whether string is a valid URI reference.
+   *
+   * 

Parses the rule: + * + *

URI-reference = URI / relative-ref
+   */
+  boolean uriReference() {
+    return this.uri() || this.relativeRef();
+  }
+
+  /**
+   * Determines whether the current position is a valid relative reference.
+   *
+   * 

Parses the rule: + * + *

relative-ref = relative-part [ "?" query ] [ "#" fragment ].
+   */
+  private boolean relativeRef() {
+    int start = this.index;
+
+    if (!this.relativePart()) {
+      return false;
+    }
+
+    if (this.take('?') && !this.query()) {
+      this.index = start;
+      return false;
+    }
+
+    if (this.take('#') && !this.fragment()) {
+      this.index = start;
+      return false;
+    }
+
+    if (this.index != this.str.length()) {
+      this.index = start;
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Determines whether the current position is a valid relative part.
+   *
+   * 

Parses the rule: + * + *

relative-part = "//" authority path-abempty
+   *                    / path-absolute
+   *                    / path-noscheme
+   *                    / path-empty
+   */
+  private boolean relativePart() {
+    int start = this.index;
+
+    if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) {
+      return true;
+    }
+
+    this.index = start;
+
+    return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty();
+  }
+
+  private boolean takeDoubleSlash() {
+    boolean isSlash = take('/');
+
+    return isSlash && take('/');
+  }
+
+  /**
+   * Determines whether the current position is a valid scheme.
+   *
+   * 

Parses the rule: + * + *

scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+   */
+  private boolean scheme() {
+    int start = this.index;
+
+    if (this.alpha()) {
+      while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) {}
+
+      if (this.str.charAt(this.index) == ':') {
+        return true;
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid authority.
+   *
+   * 

Parses the rule: + * + *

authority = [ userinfo "@" ] host [ ":" port ]
+   *
+   * Lead by double slash ("") and terminated by "/", "?", "#", or end of URI.
+   */
+  private boolean authority() {
+    int start = this.index;
+
+    if (this.userinfo()) {
+      if (!this.take('@')) {
+        this.index = start;
+        return false;
+      }
+    }
+
+    if (!this.host()) {
+      this.index = start;
+      return false;
+    }
+
+    if (this.take(':')) {
+      if (!this.port()) {
+        this.index = start;
+        return false;
+      }
+    }
+
+    if (!this.isAuthorityEnd()) {
+      this.index = start;
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Determines whether the current position is the end of the authority.
+   *
+   * 

The authority component [...] is terminated by one of the following: + * + *

    + *
  • the next slash ("/") + *
  • question mark ("?") + *
  • number sign ("#") character + *
  • the end of the URI. + *
+ */ + private boolean isAuthorityEnd() { + if (this.index >= this.str.length()) { + return true; + } + char c = this.str.charAt(this.index); + return (c == '?' || c == '#' || c == '/'); + } + + /** + * Determines whether the current position is a valid userinfo. + * + *

Parses the rule: + * + *

userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+   *
+   * Terminated by "@" in authority.
+   */
+  private boolean userinfo() {
+    int start = this.index;
+
+    while (true) {
+      if (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take(':')) {
+        continue;
+      }
+
+      if (this.index < this.str.length()) {
+        if (this.str.charAt(this.index) == '@') {
+          return true;
+        }
+      }
+
+      this.index = start;
+
+      return false;
+    }
+  }
+
+  private static int unhex(char c) {
+    if ('0' <= c && c <= '9') {
+      return c - '0';
+    } else if ('a' <= c && c <= 'f') {
+      return c - 'a' + 10;
+    } else if ('A' <= c && c <= 'F') {
+      return c - 'A' + 10;
+    }
+
+    return 0;
+  }
+
+  /**
+   * Verifies that str is correctly percent-encoded.
+   *
+   * 

Note that we essentially want to mimic the behavior of decodeURIComponent, which would fail + * on malformed URLs. Java does have various methods for decoding URLs, but none behave + * consistently with decodeURIComponent. + * + *

The code below is a combination of `checkHostPctEncoded` from the protovalidate-go + * implementation and Java's java.net.URI#decode methods. + */ + private boolean checkHostPctEncoded(String str) { + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); + + int strLen = str.length(); + ByteBuffer buffer = ByteBuffer.allocate(strLen); + CharBuffer out = CharBuffer.allocate(strLen); + + // Unhex str and convert to a ByteBuffer. + for (int i = 0; i < str.length(); ) { + if (str.charAt(i) == '%') { + // If we encounter a %, unhex the two following digits, extract their + // last 4 bits, cast to a byte. + byte b = + (byte) + (((unhex(str.charAt(i + 1)) & 0xf) << 4) | ((unhex(str.charAt(i + 2)) & 0xf) << 0)); + buffer.put(b); + i += 3; + } else { + // Not percent encoded, extract the last 4 bits, convert to a byte + // and add to the byte buffer. + buffer.put((byte) (str.charAt(i) & 0xf)); + i++; + } + } + + // Attempt to decode the byte buffer as UTF-8. + CoderResult f = decoder.decode((ByteBuffer) buffer.flip(), out, true); + + // If an error occurred, return false as invalid. + if (f.isError()) { + return false; + } + // Flush the buffer + f = decoder.flush(out); + + // If an error occurred, return false as invalid. + // Otherwise return true. + return !f.isError(); + } + + /** + * Determines whether the current position is a valid host. + * + *

Parses the rule: + * + *

host = IP-literal / IPv4address / reg-name.
+   */
+  private boolean host() {
+    if (this.index >= this.str.length()) {
+      return true;
+    }
+
+    int start = this.index;
+    this.pctEncodedFound = false;
+
+    // Note: IPv4address is a subset of reg-name
+    if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) {
+      if (this.pctEncodedFound) {
+        String rawHost = this.str.substring(start, this.index);
+        // RFC 3986:
+        // > URI producing applications must not use percent-encoding in host
+        // > unless it is used to represent a UTF-8 character sequence.
+        if (!this.checkHostPctEncoded(rawHost)) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid port.
+   *
+   * 

Parses the rule: + * + *

port = *DIGIT
+   *
+   * Terminated by end of authority.
+   */
+  private boolean port() {
+    int start = this.index;
+
+    while (true) {
+      if (this.digit()) {
+        continue;
+      }
+
+      if (this.isAuthorityEnd()) {
+        return true;
+      }
+
+      this.index = start;
+
+      return false;
+    }
+  }
+
+  /**
+   * Determines whether the current position is a valid IP literal.
+   *
+   * 

Parses the rule from RFC 6874: + * + *

IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"
+   */
+  private boolean ipLiteral() {
+    int start = this.index;
+
+    if (this.take('[')) {
+      int j = this.index;
+
+      if (this.ipv6Address() && this.take(']')) {
+        return true;
+      }
+
+      this.index = j;
+
+      if (this.ipv6Addrz() && this.take(']')) {
+        return true;
+      }
+
+      this.index = j;
+
+      if (this.ipvFuture() && this.take(']')) {
+        return true;
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid ipv6 address.
+   *
+   * 

ipv6Address parses the rule "IPv6address". + * + *

Relies on the implementation of isIp. + */ + private boolean ipv6Address() { + int start = this.index; + + while (this.hexDig() || this.take(':')) {} + + if (CustomOverload.isIp(this.str.substring(start, this.index), 6)) { + return true; + } + + this.index = start; + + return false; + } + + /** + * Determines whether the current position is a valid IPv6addrz. + * + * Parses the rule: + * + *

IPv6addrz = IPv6address "%25" ZoneID
+   */
+  private boolean ipv6Addrz() {
+    int start = this.index;
+
+    if (this.ipv6Address() && this.take('%') && this.take('2') && this.take('5') && this.zoneID()) {
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid zone ID.
+   *
+   * Parses the rule:
+   *
+   * 
ZoneID = 1*( unreserved / pct-encoded )
+   */
+  private boolean zoneID() {
+    int start = this.index;
+
+    while (this.unreserved() || this.pctEncoded()) {}
+
+    if (this.index - start > 0) {
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid IPvFuture.
+   *
+   * Parses the rule:
+   *
+   * 
IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+   */
+  private boolean ipvFuture() {
+    int start = this.index;
+
+    if (this.take('v') && this.hexDig()) {
+      while (this.hexDig()) {}
+
+      if (this.take('.')) {
+        int j = 0;
+
+        while (this.unreserved() || this.subDelims() || this.take(':')) {
+          j++;
+        }
+
+        if (j >= 1) {
+          return true;
+        }
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid reg-name.
+   *
+   * Parses the rule:
+   *
+   * 
reg-name = *( unreserved / pct-encoded / sub-delims )
+   *
+   * Terminates on start of port (":") or end of authority.
+   */
+  private boolean regName() {
+    int start = this.index;
+
+    while (true) {
+      if (this.unreserved() || this.pctEncoded() || this.subDelims()) {
+        continue;
+      }
+
+      if (this.isAuthorityEnd()) {
+        // End of authority
+        return true;
+      }
+
+      if (this.str.charAt(this.index) == ':') {
+        return true;
+      }
+
+      this.index = start;
+
+      return false;
+    }
+  }
+
+  /**
+   * Determines whether the current position is the end of the path.
+   *
+   * 

The path is terminated by one of the following: + * + *

    + *
  • the first question mark ("?") + *
  • number sign ("#") character + *
  • the end of the URI. + *
+ */ + private boolean isPathEnd() { + if (this.index >= this.str.length()) { + return true; + } + + char c = this.str.charAt(this.index); + + return (c == '?' || c == '#'); + } + + /** + * Determines whether the current position is a valid path-abempty. + * + * Parses the rule: + * + *
path-abempty = *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
+  private boolean pathAbempty() {
+    int start = this.index;
+
+    while (this.take('/') && this.segment()) {}
+
+    if (this.isPathEnd()) {
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid path-absolute.
+   *
+   * Parses the rule:
+   *
+   * 
path-absolute = "/" [ segment-nz *( "/" segment ) ]
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
+  private boolean pathAbsolute() {
+    int start = this.index;
+
+    if (this.take('/')) {
+      if (this.segmentNz()) {
+        while (this.take('/') && this.segment()) {}
+      }
+
+      if (this.isPathEnd()) {
+        return true;
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid path-noscheme.
+   *
+   * Parses the rule:
+   *
+   * 
path-noscheme = segment-nz-nc *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
+  private boolean pathNoscheme() {
+    int start = this.index;
+
+    if (this.segmentNzNc()) {
+      while (this.take('/') && this.segment()) {}
+
+      if (this.isPathEnd()) {
+        return true;
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid path-rootless.
+   *
+   * Parses the rule:
+   *
+   * 
path-rootless = segment-nz *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
+  private boolean pathRootless() {
+    int start = this.index;
+
+    if (this.segmentNz()) {
+      while (this.take('/') && this.segment()) {}
+
+      if (this.isPathEnd()) {
+        return true;
+      }
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid path-empty.
+   *
+   * Parses the rule:
+   *
+   * 
path-empty = 0
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
+  private boolean pathEmpty() {
+    return this.isPathEnd();
+  }
+
+  /**
+   * Determines whether the current position is a valid segment.
+   *
+   * Parses the rule:
+   *
+   * 
segment = *pchar
+   */
+  private boolean segment() {
+    while (this.pchar()) {}
+
+    return true;
+  }
+
+  /**
+   * Determines whether the current position is a valid segment-nz.
+   *
+   * Parses the rule:
+   *
+   * 
segment-nz = 1*pchar
+   */
+  private boolean segmentNz() {
+    int start = this.index;
+
+    if (this.pchar()) {
+      while (this.pchar()) {}
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid segment-nz-nc.
+   *
+   * Parses the rule:
+   *
+   * 
segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+   *                   ; non-zero-length segment without any colon ":"
+   */
+  private boolean segmentNzNc() {
+    int start = this.index;
+
+    while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) {}
+
+    if (this.index - start > 0) {
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a valid pchar.
+   *
+   * Parses the rule:
+   *
+   * 
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+   */
+  private boolean pchar() {
+    return (this.unreserved()
+        || this.pctEncoded()
+        || this.subDelims()
+        || this.take(':')
+        || this.take('@'));
+  }
+
+  /**
+   * Determines whether the current position is a valid query.
+   *
+   * Parses the rule:
+   *
+   * 
query = *( pchar / "/" / "?" )
+   *
+   * Terminated by "#" or end of URI.
+   */
+  private boolean query() {
+    int start = this.index;
+
+    while (true) {
+      if (this.pchar() || this.take('/') || this.take('?')) {
+        continue;
+      }
+
+      if (this.index == this.str.length() || this.str.charAt(this.index) == '#') {
+        return true;
+      }
+
+      this.index = start;
+
+      return false;
+    }
+  }
+
+  /**
+   * Determines whether the current position is a valid fragment.
+   *
+   * Parses the rule:
+   *
+   * 
fragment = *( pchar / "/" / "?" )
+   *
+   * Terminated by end of URI.
+   */
+  private boolean fragment() {
+    int start = this.index;
+
+    while (true) {
+      if (this.pchar() || this.take('/') || this.take('?')) {
+        continue;
+      }
+
+      if (this.index == this.str.length()) {
+        return true;
+      }
+
+      this.index = start;
+
+      return false;
+    }
+  }
+
+  /**
+   * Determines whether the current position is a valid pct-encoded.
+   *
+   * Parses the rule:
+   *
+   * 
pct-encoded = "%"+HEXDIG+HEXDIG
+   *
+   * Sets `pctEncodedFound` to true if a valid triplet was found.
+   */
+  private boolean pctEncoded() {
+    int start = this.index;
+
+    if (this.take('%') && this.hexDig() && this.hexDig()) {
+      this.pctEncodedFound = true;
+
+      return true;
+    }
+
+    this.index = start;
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is an unreserved character.
+   *
+   * Parses the rule:
+   *
+   * 
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+   */
+  private boolean unreserved() {
+    return (this.alpha()
+        || this.digit()
+        || this.take('-')
+        || this.take('_')
+        || this.take('.')
+        || this.take('~'));
+  }
+
+  /**
+   * Determines whether the current position is a sub-delim.
+   *
+   * Parses the rule:
+   *
+   * 
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
+   *                  / "*" / "+" / "," / ";" / "="
+   */
+  private boolean subDelims() {
+    return (this.take('!')
+        || this.take('$')
+        || this.take('&')
+        || this.take('\'')
+        || this.take('(')
+        || this.take(')')
+        || this.take('*')
+        || this.take('+')
+        || this.take(',')
+        || this.take(';')
+        || this.take('='));
+  }
+
+  /**
+   * Determines whether the current position is an alpha character.
+   *
+   * Parses the rule:
+   *
+   * 
ALPHA =  %x41-5A / %x61-7A ; A-Z / a-z
+   */
+  private boolean alpha() {
+    if (this.index >= this.str.length()) {
+      return false;
+    }
+
+    char c = this.str.charAt(this.index);
+
+    if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
+      this.index++;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a hex digit.
+   *
+   * 

Parses the rule: + * + *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+   */
+  private boolean hexDig() {
+    if (this.index >= this.str.length()) {
+      return false;
+    }
+
+    char c = this.str.charAt(this.index);
+
+    if (('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')) {
+      this.index++;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines whether the current position is a digit.
+   *
+   * 

Parses the rule: + * + *

DIGIT = %x30-39 ; 0-9
+   */
+  private boolean digit() {
+    if (this.index >= this.str.length()) {
+      return false;
+    }
+
+    char c = this.str.charAt(this.index);
+    if ('0' <= c && c <= '9') {
+      this.index++;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Take the given char at the current position, incrementing the index if necessary.
+   */
+  private boolean take(char c) {
+    if (this.index >= this.str.length()) {
+      return false;
+    }
+
+    if (this.str.charAt(this.index) == c) {
+      this.index++;
+      return true;
+    }
+
+    return false;
+  }
+}

From ec096a04b664c1055ecad810d3b6df75fdba4268 Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Wed, 9 Apr 2025 10:36:11 -0400
Subject: [PATCH 04/12] Lint

---
 src/main/java/build/buf/protovalidate/Ipv4.java | 7 +++----
 src/main/java/build/buf/protovalidate/Ipv6.java | 7 +++----
 src/main/java/build/buf/protovalidate/Uri.java  | 8 ++------
 3 files changed, 8 insertions(+), 14 deletions(-)

diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java
index c3726b64..538c38ae 100644
--- a/src/main/java/build/buf/protovalidate/Ipv4.java
+++ b/src/main/java/build/buf/protovalidate/Ipv4.java
@@ -18,7 +18,8 @@
 import java.util.List;
 
 /**
- * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address prefix.
+ * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address
+ * prefix.
  */
 final class Ipv4 {
   private String str;
@@ -187,9 +188,7 @@ private boolean digit() {
     return false;
   }
 
-  /**
-   * Take the given char at the current position, incrementing the index if necessary.
-   */
+  /** Take the given char at the current position, incrementing the index if necessary. */
   private boolean take(char c) {
     if (this.index >= this.str.length()) {
       return false;
diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java
index db14eb68..52c276c0 100644
--- a/src/main/java/build/buf/protovalidate/Ipv6.java
+++ b/src/main/java/build/buf/protovalidate/Ipv6.java
@@ -19,7 +19,8 @@
 import javax.annotation.Nullable;
 
 /**
- * Ipv6 is a class used to parse a given string to determine if it is an IPv6 address or address prefix.
+ * Ipv6 is a class used to parse a given string to determine if it is an IPv6 address or address
+ * prefix.
  */
 final class Ipv6 {
   private String str;
@@ -328,9 +329,7 @@ private boolean digit() {
     return false;
   }
 
-  /**
-   * Take the given char at the current position, incrementing the index if necessary.
-   */
+  /** Take the given char at the current position, incrementing the index if necessary. */
   private boolean take(char c) {
     if (this.index >= this.str.length()) {
       return false;
diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java
index 63287135..65fd63ed 100644
--- a/src/main/java/build/buf/protovalidate/Uri.java
+++ b/src/main/java/build/buf/protovalidate/Uri.java
@@ -20,9 +20,7 @@
 import java.nio.charset.CoderResult;
 import java.nio.charset.StandardCharsets;
 
-/**
- * Ipv6 is a class used to parse a given string to determine if it is a URI or URI reference.
- */
+/** Ipv6 is a class used to parse a given string to determine if it is a URI or URI reference. */
 final class Uri {
   private String str;
   private int index;
@@ -931,9 +929,7 @@ private boolean digit() {
     return false;
   }
 
-  /**
-   * Take the given char at the current position, incrementing the index if necessary.
-   */
+  /** Take the given char at the current position, incrementing the index if necessary. */
   private boolean take(char c) {
     if (this.index >= this.str.length()) {
       return false;

From 38be4e6a60b0af80d844b1f3dc439afd82b6bd72 Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Wed, 16 Apr 2025 10:53:32 -0400
Subject: [PATCH 05/12] Fix ipv6 validation corner cases (#260)

This ports the ipv6 fix from
https://github.com/bufbuild/protovalidate-go/pull/215.

For context see the description on the above PR. The summary is that
this fixes the validation for some corner cases of IPv6 address
validation. Namely:

* Adds a check that an IPv6 address can't begin or end on a single
colon.
* Adds a check to fail-fast on invalid hextets.
---
 .../java/build/buf/protovalidate/Ipv6.java    | 40 ++++++++++++-------
 1 file changed, 26 insertions(+), 14 deletions(-)

diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java
index 52c276c0..239cd894 100644
--- a/src/main/java/build/buf/protovalidate/Ipv6.java
+++ b/src/main/java/build/buf/protovalidate/Ipv6.java
@@ -176,8 +176,12 @@ private boolean addressPart() {
         return false;
       }
 
-      if (this.h16()) {
-        continue;
+      try {
+        if (this.h16()) {
+          continue;
+        }
+      } catch (IllegalStateException | NumberFormatException e) {
+        return false;
       }
 
       if (this.take(':')) {
@@ -191,6 +195,9 @@ private boolean addressPart() {
           if (this.take(':')) {
             return false;
           }
+        } else if (this.index == 1 || this.index == this.str.length()) {
+          // invalid - string cannot start or end on single colon
+          return false;
         }
         continue;
       }
@@ -264,9 +271,13 @@ private boolean dotted() {
    *
    * 
h16 = 1*4HEXDIG
    *
-   * 

Stores 16-bit value in pieces. + * If 1-4 hex digits are found, the parsed 16-bit unsigned integer is stored + * in pieces and true is returned. + * If 0 hex digits are found, returns false. + * If more than 4 hex digits are found, an IllegalStateException is thrown. + * If the found hex digits cannot be converted to an int, a NumberFormatException is raised. */ - private boolean h16() { + private boolean h16() throws IllegalStateException, NumberFormatException { int start = this.index; while (this.index < this.str.length() && this.hexDig()) {} @@ -274,24 +285,25 @@ private boolean h16() { String str = this.str.substring(start, this.index); if (str.isEmpty()) { - // too short + // too short, just return false + // this is not an error condition, it just means we didn't find any + // hex digits at the current position. return false; } if (str.length() > 4) { // too long - return false; + // this is an error condition, it means we found a string of more than + // four valid hex digits, which is invalid in ipv6 addresses. + throw new IllegalStateException("invalid hex"); } - try { - int val = Integer.parseInt(str, 16); + // Note that this will throw a NumberFormatException if string cannot be + // converted to an int. + int val = Integer.parseInt(str, 16); - this.pieces.add(val); - return true; - } catch (NumberFormatException nfe) { - // Error converting to number - return false; - } + this.pieces.add(val); + return true; } /** From c1e8e82ca2c0e132f50cb8e49b862fded2c06821 Mon Sep 17 00:00:00 2001 From: Timo Stamm Date: Wed, 16 Apr 2025 18:04:09 +0200 Subject: [PATCH 06/12] Fix several issues in isHostAndPort, isHostname, and isUri (#264) --- .../buf/protovalidate/CustomOverload.java | 11 ++-- .../java/build/buf/protovalidate/Uri.java | 60 +++++++++---------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index 94cc4819..fa244fbe 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -16,7 +16,6 @@ import com.google.common.primitives.Bytes; import java.util.HashSet; -import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; import org.projectnessie.cel.common.types.BoolT; @@ -463,6 +462,10 @@ private static boolean isPort(String str) { return false; } + if (str.length() > 1 && str.charAt(0) == '0') { + return false; + } + for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if ('0' <= c && c <= '9') { @@ -557,7 +560,7 @@ private static boolean isHostname(String val) { boolean allDigits = false; - String[] parts = str.toLowerCase(Locale.getDefault()).split("\\.", -1); + String[] parts = str.split("\\.", -1); // split hostname on '.' and validate each part for (String part : parts) { @@ -572,8 +575,8 @@ private static boolean isHostname(String val) { // for each character in part for (int i = 0; i < part.length(); i++) { char c = part.charAt(i); - // if the character is not a-z, 0-9, or '-', it is invalid - if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-') { + // if the character is not a-z, A-Z, 0-9, or '-', it is invalid + if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-') { return false; } diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index 65fd63ed..60a127e2 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -167,7 +167,7 @@ private boolean scheme() { if (this.alpha()) { while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) {} - if (this.str.charAt(this.index) == ':') { + if (this.peek(':')) { return true; } } @@ -253,10 +253,8 @@ private boolean userinfo() { continue; } - if (this.index < this.str.length()) { - if (this.str.charAt(this.index) == '@') { - return true; - } + if (this.peek('@')) { + return true; } this.index = start; @@ -335,15 +333,11 @@ private boolean checkHostPctEncoded(String str) { *

host = IP-literal / IPv4address / reg-name.
    */
   private boolean host() {
-    if (this.index >= this.str.length()) {
-      return true;
-    }
-
     int start = this.index;
     this.pctEncodedFound = false;
 
     // Note: IPv4address is a subset of reg-name
-    if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) {
+    if ((this.peek('[') && this.ipLiteral()) || this.regName()) {
       if (this.pctEncodedFound) {
         String rawHost = this.str.substring(start, this.index);
         // RFC 3986:
@@ -446,7 +440,7 @@ private boolean ipv6Address() {
   /**
    * Determines whether the current position is a valid IPv6addrz.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

IPv6addrz = IPv6address "%25" ZoneID
    */
@@ -465,7 +459,7 @@ private boolean ipv6Addrz() {
   /**
    * Determines whether the current position is a valid zone ID.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

ZoneID = 1*( unreserved / pct-encoded )
    */
@@ -486,7 +480,7 @@ private boolean zoneID() {
   /**
    * Determines whether the current position is a valid IPvFuture.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
    */
@@ -517,7 +511,7 @@ private boolean ipvFuture() {
   /**
    * Determines whether the current position is a valid reg-name.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

reg-name = *( unreserved / pct-encoded / sub-delims )
    *
@@ -536,7 +530,7 @@ private boolean regName() {
         return true;
       }
 
-      if (this.str.charAt(this.index) == ':') {
+      if (this.peek(':')) {
         return true;
       }
 
@@ -570,7 +564,7 @@ private boolean isPathEnd() {
   /**
    * Determines whether the current position is a valid path-abempty.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

path-abempty = *( "/" segment )
    *
@@ -593,7 +587,7 @@ private boolean pathAbempty() {
   /**
    * Determines whether the current position is a valid path-absolute.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

path-absolute = "/" [ segment-nz *( "/" segment ) ]
    *
@@ -620,7 +614,7 @@ private boolean pathAbsolute() {
   /**
    * Determines whether the current position is a valid path-noscheme.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

path-noscheme = segment-nz-nc *( "/" segment )
    *
@@ -645,7 +639,7 @@ private boolean pathNoscheme() {
   /**
    * Determines whether the current position is a valid path-rootless.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

path-rootless = segment-nz *( "/" segment )
    *
@@ -670,7 +664,7 @@ private boolean pathRootless() {
   /**
    * Determines whether the current position is a valid path-empty.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

path-empty = 0
    *
@@ -683,7 +677,7 @@ private boolean pathEmpty() {
   /**
    * Determines whether the current position is a valid segment.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

segment = *pchar
    */
@@ -696,7 +690,7 @@ private boolean segment() {
   /**
    * Determines whether the current position is a valid segment-nz.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

segment-nz = 1*pchar
    */
@@ -716,7 +710,7 @@ private boolean segmentNz() {
   /**
    * Determines whether the current position is a valid segment-nz-nc.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
    *                   ; non-zero-length segment without any colon ":"
@@ -738,7 +732,7 @@ private boolean segmentNzNc() {
   /**
    * Determines whether the current position is a valid pchar.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
    */
@@ -753,7 +747,7 @@ private boolean pchar() {
   /**
    * Determines whether the current position is a valid query.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

query = *( pchar / "/" / "?" )
    *
@@ -767,7 +761,7 @@ private boolean query() {
         continue;
       }
 
-      if (this.index == this.str.length() || this.str.charAt(this.index) == '#') {
+      if (this.peek('#') || this.index == this.str.length()) {
         return true;
       }
 
@@ -780,7 +774,7 @@ private boolean query() {
   /**
    * Determines whether the current position is a valid fragment.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

fragment = *( pchar / "/" / "?" )
    *
@@ -807,7 +801,7 @@ private boolean fragment() {
   /**
    * Determines whether the current position is a valid pct-encoded.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

pct-encoded = "%"+HEXDIG+HEXDIG
    *
@@ -830,7 +824,7 @@ private boolean pctEncoded() {
   /**
    * Determines whether the current position is an unreserved character.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
    */
@@ -846,7 +840,7 @@ private boolean unreserved() {
   /**
    * Determines whether the current position is a sub-delim.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
    *                  / "*" / "+" / "," / ";" / "="
@@ -868,7 +862,7 @@ private boolean subDelims() {
   /**
    * Determines whether the current position is an alpha character.
    *
-   * Parses the rule:
+   * 

Parses the rule: * *

ALPHA =  %x41-5A / %x61-7A ; A-Z / a-z
    */
@@ -942,4 +936,8 @@ private boolean take(char c) {
 
     return false;
   }
+
+  private boolean peek(char c) {
+    return this.index < this.str.length() && this.str.charAt(this.index) == c;
+  }
 }

From 8c533350bb0347b26e3c199986611433a336a73c Mon Sep 17 00:00:00 2001
From: "Philip K. Warren" 
Date: Mon, 21 Apr 2025 13:01:10 -0500
Subject: [PATCH 07/12] Remove guava dependency (#265)

---
 build.gradle.kts                              |   4 +-
 conformance/build.gradle.kts                  |  10 +-
 .../buf/protovalidate/conformance/Main.java   |   9 +-
 gradle/libs.versions.toml                     |   6 +-
 .../build/buf/protovalidate/CelPrograms.java  |   2 +-
 .../buf/protovalidate/CompiledProgram.java    |   5 +-
 .../buf/protovalidate/ConstraintCache.java    |   4 +-
 .../protovalidate/ConstraintViolation.java    |   2 +-
 .../ConstraintViolationHelper.java            |   5 +-
 .../buf/protovalidate/CustomOverload.java     |  25 ++-
 .../buf/protovalidate/DescriptorMappings.java |   2 +-
 .../buf/protovalidate/EvaluatorBuilder.java   |  20 +-
 .../buf/protovalidate/FieldEvaluator.java     |   2 +-
 .../buf/protovalidate/FieldPathUtils.java     |   2 +-
 .../java/build/buf/protovalidate/Ipv6.java    |   2 +-
 .../build/buf/protovalidate/MessageValue.java |   4 +-
 .../build/buf/protovalidate/NowVariable.java  |   2 +-
 .../build/buf/protovalidate/ObjectValue.java  |   2 +-
 .../java/build/buf/protovalidate/Value.java   |   8 +-
 .../buf/protovalidate/ValueEvaluator.java     |   8 +-
 .../build/buf/protovalidate/Variable.java     |   2 +-
 .../build/buf/protovalidate/Violation.java    |  11 +-
 .../buf/protovalidate/CustomOverloadTest.java | 209 ++++++++----------
 23 files changed, 167 insertions(+), 179 deletions(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index a53e6d86..c50440c4 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -257,10 +257,10 @@ mavenPublishing {
 
 dependencies {
     annotationProcessor(libs.nullaway)
+    api(libs.jspecify)
     api(libs.protobuf.java)
     implementation(enforcedPlatform(libs.cel))
     implementation(libs.cel.core)
-    implementation(libs.guava)
 
     buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe")
 
@@ -269,5 +269,5 @@ dependencies {
     testImplementation("org.junit.jupiter:junit-jupiter")
     testRuntimeOnly("org.junit.platform:junit-platform-launcher")
 
-    errorprone(libs.errorprone)
+    errorprone(libs.errorprone.core)
 }
diff --git a/conformance/build.gradle.kts b/conformance/build.gradle.kts
index a69c45f3..64a3789b 100644
--- a/conformance/build.gradle.kts
+++ b/conformance/build.gradle.kts
@@ -10,6 +10,12 @@ plugins {
     alias(libs.plugins.osdetector)
 }
 
+// Conformance tests aren't bound by lowest common library version.
+java {
+    sourceCompatibility = JavaVersion.VERSION_21
+    targetCompatibility = JavaVersion.VERSION_21
+}
+
 val buf: Configuration by configurations.creating
 
 tasks.register("configureBuf") {
@@ -116,7 +122,7 @@ configure {
 
 dependencies {
     implementation(project(":"))
-    implementation(libs.guava)
+    implementation(libs.errorprone.annotations)
     implementation(libs.protobuf.java)
 
     implementation(libs.assertj)
@@ -127,5 +133,5 @@ dependencies {
     testImplementation("org.junit.jupiter:junit-jupiter")
     testRuntimeOnly("org.junit.platform:junit-platform-launcher")
 
-    errorprone(libs.errorprone)
+    errorprone(libs.errorprone.core)
 }
diff --git a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java b/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java
index a59e0de9..9c2cd915 100644
--- a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java
+++ b/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java
@@ -24,7 +24,6 @@
 import build.buf.validate.conformance.harness.TestConformanceRequest;
 import build.buf.validate.conformance.harness.TestConformanceResponse;
 import build.buf.validate.conformance.harness.TestResult;
-import com.google.common.base.Splitter;
 import com.google.errorprone.annotations.FormatMethod;
 import com.google.protobuf.Any;
 import com.google.protobuf.ByteString;
@@ -34,7 +33,6 @@
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.protobuf.TypeRegistry;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 public class Main {
@@ -84,8 +82,11 @@ static TestConformanceResponse testConformance(TestConformanceRequest request) {
   static TestResult testCase(
       Validator validator, Map fileDescriptors, Any testCase)
       throws InvalidProtocolBufferException {
-    List urlParts = Splitter.on('/').limit(2).splitToList(testCase.getTypeUrl());
-    String fullName = urlParts.get(urlParts.size() - 1);
+    String fullName = testCase.getTypeUrl();
+    int slash = fullName.indexOf('/');
+    if (slash != -1) {
+      fullName = fullName.substring(slash + 1);
+    }
     Descriptors.Descriptor descriptor = fileDescriptors.get(fullName);
     if (descriptor == null) {
       return unexpectedErrorResult("Unable to find descriptor: %s", fullName);
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 610b257a..21ac837c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,7 @@
 assertj = "3.27.3"
 buf = "1.52.1"
 cel = "0.5.1"
+error-prone = "2.37.0"
 junit = "5.12.1"
 maven-publish = "0.31.0"
 # When updating, make sure to update versions in the following files to match and regenerate code with 'make generate'.
@@ -16,8 +17,9 @@ assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
 buf = { module = "build.buf:buf", version.ref = "buf" }
 cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" }
 cel-core = { module = "org.projectnessie.cel:cel-core" }
-errorprone = { module = "com.google.errorprone:error_prone_core", version = "2.37.0" }
-guava = { module = "com.google.guava:guava", version = "33.4.0-jre" }
+errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" }
+errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" }
+jspecify = { module ="org.jspecify:jspecify", version = "1.0.0" }
 junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
 maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" }
 nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.6" }
diff --git a/src/main/java/build/buf/protovalidate/CelPrograms.java b/src/main/java/build/buf/protovalidate/CelPrograms.java
index a3df4ed6..38bcc097 100644
--- a/src/main/java/build/buf/protovalidate/CelPrograms.java
+++ b/src/main/java/build/buf/protovalidate/CelPrograms.java
@@ -17,7 +17,7 @@
 import build.buf.protovalidate.exceptions.ExecutionException;
 import java.util.ArrayList;
 import java.util.List;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /** Evaluator that executes a {@link CompiledProgram}. */
 class CelPrograms implements Evaluator {
diff --git a/src/main/java/build/buf/protovalidate/CompiledProgram.java b/src/main/java/build/buf/protovalidate/CompiledProgram.java
index fa1c68a8..d9343ccc 100644
--- a/src/main/java/build/buf/protovalidate/CompiledProgram.java
+++ b/src/main/java/build/buf/protovalidate/CompiledProgram.java
@@ -16,7 +16,7 @@
 
 import build.buf.protovalidate.exceptions.ExecutionException;
 import build.buf.validate.FieldPath;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.Program;
 import org.projectnessie.cel.common.types.Err;
 import org.projectnessie.cel.common.types.ref.Val;
@@ -63,8 +63,7 @@ public CompiledProgram(
    *     violations.
    * @throws ExecutionException If the evaluation of the CEL program fails with an error.
    */
-  @Nullable
-  public ConstraintViolation.Builder eval(Value fieldValue, Variable bindings)
+  public ConstraintViolation.@Nullable Builder eval(Value fieldValue, Variable bindings)
       throws ExecutionException {
     Program.EvalResult evalResult = program.eval(bindings);
     Val val = evalResult.getVal();
diff --git a/src/main/java/build/buf/protovalidate/ConstraintCache.java b/src/main/java/build/buf/protovalidate/ConstraintCache.java
index 708019f2..740b25a3 100644
--- a/src/main/java/build/buf/protovalidate/ConstraintCache.java
+++ b/src/main/java/build/buf/protovalidate/ConstraintCache.java
@@ -32,7 +32,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.Ast;
 import org.projectnessie.cel.Env;
 import org.projectnessie.cel.EnvOption;
@@ -199,7 +199,7 @@ public List compile(
     return celRules;
   }
 
-  private @Nullable build.buf.validate.PredefinedConstraints getFieldConstraints(
+  private build.buf.validate.@Nullable PredefinedConstraints getFieldConstraints(
       FieldDescriptor constraintFieldDesc) throws CompilationException {
     DescriptorProtos.FieldOptions options = constraintFieldDesc.getOptions();
     // If the protovalidate field option is unknown, reparse options using our extension registry.
diff --git a/src/main/java/build/buf/protovalidate/ConstraintViolation.java b/src/main/java/build/buf/protovalidate/ConstraintViolation.java
index 4b46ddf4..c141d1ff 100644
--- a/src/main/java/build/buf/protovalidate/ConstraintViolation.java
+++ b/src/main/java/build/buf/protovalidate/ConstraintViolation.java
@@ -23,7 +23,7 @@
 import java.util.Deque;
 import java.util.List;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@link ConstraintViolation} contains all of the collected information about an individual
diff --git a/src/main/java/build/buf/protovalidate/ConstraintViolationHelper.java b/src/main/java/build/buf/protovalidate/ConstraintViolationHelper.java
index 2a9b4943..05e7ae28 100644
--- a/src/main/java/build/buf/protovalidate/ConstraintViolationHelper.java
+++ b/src/main/java/build/buf/protovalidate/ConstraintViolationHelper.java
@@ -18,7 +18,7 @@
 import build.buf.validate.FieldPathElement;
 import java.util.ArrayList;
 import java.util.List;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 class ConstraintViolationHelper {
   private static final List EMPTY_PREFIX = new ArrayList<>();
@@ -46,8 +46,7 @@ class ConstraintViolationHelper {
     this.fieldPathElement = null;
   }
 
-  @Nullable
-  FieldPathElement getFieldPathElement() {
+  @Nullable FieldPathElement getFieldPathElement() {
     return fieldPathElement;
   }
 
diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java
index fa244fbe..968ad5fa 100644
--- a/src/main/java/build/buf/protovalidate/CustomOverload.java
+++ b/src/main/java/build/buf/protovalidate/CustomOverload.java
@@ -14,7 +14,6 @@
 
 package build.buf.protovalidate;
 
-import com.google.common.primitives.Bytes;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -205,12 +204,34 @@ private static Overload celContains() {
           if (lhsType == TypeEnum.Bytes) {
             byte[] receiver = (byte[]) lhs.value();
             byte[] param = (byte[]) rhs.value();
-            return Types.boolOf(Bytes.indexOf(receiver, param) != -1);
+            return Types.boolOf(bytesContains(receiver, param));
           }
           return Err.noSuchOverload(lhs, OVERLOAD_CONTAINS, rhs);
         });
   }
 
+  static boolean bytesContains(byte[] arr, byte[] subArr) {
+    if (subArr.length == 0) {
+      return true;
+    }
+    if (subArr.length > arr.length) {
+      return false;
+    }
+    for (int i = 0; i < arr.length - subArr.length + 1; i++) {
+      boolean found = true;
+      for (int j = 0; j < subArr.length; j++) {
+        if (arr[i + j] != subArr[j]) {
+          found = false;
+          break;
+        }
+      }
+      if (found) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * Creates a custom binary function overload for the "isHostname" operation.
    *
diff --git a/src/main/java/build/buf/protovalidate/DescriptorMappings.java b/src/main/java/build/buf/protovalidate/DescriptorMappings.java
index d6f33497..20dd9490 100644
--- a/src/main/java/build/buf/protovalidate/DescriptorMappings.java
+++ b/src/main/java/build/buf/protovalidate/DescriptorMappings.java
@@ -21,7 +21,7 @@
 import com.google.protobuf.Descriptors.OneofDescriptor;
 import java.util.HashMap;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.checker.Decls;
 
 /**
diff --git a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
index 6b25545d..538bf2bd 100644
--- a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
+++ b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
@@ -22,7 +22,6 @@
 import build.buf.validate.Ignore;
 import build.buf.validate.MessageConstraints;
 import build.buf.validate.OneofConstraints;
-import com.google.common.collect.ImmutableMap;
 import com.google.protobuf.ByteString;
 import com.google.protobuf.Descriptors;
 import com.google.protobuf.Descriptors.Descriptor;
@@ -35,8 +34,9 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.Env;
 import org.projectnessie.cel.EnvOption;
 import org.projectnessie.cel.checker.Decls;
@@ -47,7 +47,7 @@ class EvaluatorBuilder {
       FieldPathUtils.fieldPathElement(
           FieldConstraints.getDescriptor().findFieldByNumber(FieldConstraints.CEL_FIELD_NUMBER));
 
-  private volatile ImmutableMap evaluatorCache = ImmutableMap.of();
+  private volatile Map evaluatorCache = Collections.emptyMap();
 
   private final Env env;
   private final boolean disableLazy;
@@ -98,7 +98,7 @@ private Evaluator build(Descriptor desc) throws CompilationException {
         return eval;
       }
       // Rebuild cache with this descriptor (and any of its dependencies).
-      ImmutableMap updatedCache =
+      Map updatedCache =
           new DescriptorCacheBuilder(env, constraints, evaluatorCache).build(desc);
       evaluatorCache = updatedCache;
       eval = updatedCache.get(desc);
@@ -117,9 +117,7 @@ private static class DescriptorCacheBuilder {
     private final HashMap cache;
 
     private DescriptorCacheBuilder(
-        Env env,
-        ConstraintCache constraintCache,
-        ImmutableMap previousCache) {
+        Env env, ConstraintCache constraintCache, Map previousCache) {
       this.env = Objects.requireNonNull(env, "env");
       this.constraintCache = Objects.requireNonNull(constraintCache, "constraintCache");
       this.cache = new HashMap<>(previousCache);
@@ -130,13 +128,13 @@ private DescriptorCacheBuilder(
      * references).
      *
      * @param descriptor Descriptor used to build the cache.
-     * @return Immutable map of descriptors to evaluators.
+     * @return Unmodifiable map of descriptors to evaluators.
      * @throws CompilationException If an error occurs compiling a constraint on the cache.
      */
-    public ImmutableMap build(Descriptor descriptor)
+    public Map build(Descriptor descriptor)
         throws CompilationException {
       createMessageEvaluator(descriptor);
-      return ImmutableMap.copyOf(cache);
+      return Collections.unmodifiableMap(cache);
     }
 
     private MessageEvaluator createMessageEvaluator(Descriptor desc) throws CompilationException {
@@ -234,12 +232,10 @@ private FieldEvaluator buildField(
       return fieldEvaluator;
     }
 
-    @SuppressWarnings("deprecation")
     private boolean shouldSkip(FieldConstraints constraints) {
       return constraints.getIgnore() == Ignore.IGNORE_ALWAYS;
     }
 
-    @SuppressWarnings("deprecation")
     private static boolean shouldIgnoreEmpty(FieldConstraints constraints) {
       return constraints.getIgnore() == Ignore.IGNORE_IF_UNPOPULATED
           || constraints.getIgnore() == Ignore.IGNORE_IF_DEFAULT_VALUE;
diff --git a/src/main/java/build/buf/protovalidate/FieldEvaluator.java b/src/main/java/build/buf/protovalidate/FieldEvaluator.java
index c8069a67..3d4e23b7 100644
--- a/src/main/java/build/buf/protovalidate/FieldEvaluator.java
+++ b/src/main/java/build/buf/protovalidate/FieldEvaluator.java
@@ -22,7 +22,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /** Performs validation on a single message field, defined by its descriptor. */
 class FieldEvaluator implements Evaluator {
diff --git a/src/main/java/build/buf/protovalidate/FieldPathUtils.java b/src/main/java/build/buf/protovalidate/FieldPathUtils.java
index aca207be..b07664e8 100644
--- a/src/main/java/build/buf/protovalidate/FieldPathUtils.java
+++ b/src/main/java/build/buf/protovalidate/FieldPathUtils.java
@@ -18,7 +18,7 @@
 import build.buf.validate.FieldPathElement;
 import com.google.protobuf.Descriptors;
 import java.util.List;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /** Utility class for manipulating error paths in violations. */
 final class FieldPathUtils {
diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java
index 239cd894..7e036292 100644
--- a/src/main/java/build/buf/protovalidate/Ipv6.java
+++ b/src/main/java/build/buf/protovalidate/Ipv6.java
@@ -16,7 +16,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Ipv6 is a class used to parse a given string to determine if it is an IPv6 address or address
diff --git a/src/main/java/build/buf/protovalidate/MessageValue.java b/src/main/java/build/buf/protovalidate/MessageValue.java
index 9ec965d7..de57f26d 100644
--- a/src/main/java/build/buf/protovalidate/MessageValue.java
+++ b/src/main/java/build/buf/protovalidate/MessageValue.java
@@ -19,7 +19,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /** The {@link Value} type that contains a {@link com.google.protobuf.Message}. */
 final class MessageValue implements Value {
@@ -37,7 +37,7 @@ public MessageValue(Message value) {
   }
 
   @Override
-  public @Nullable Descriptors.FieldDescriptor fieldDescriptor() {
+  public Descriptors.@Nullable FieldDescriptor fieldDescriptor() {
     return null;
   }
 
diff --git a/src/main/java/build/buf/protovalidate/NowVariable.java b/src/main/java/build/buf/protovalidate/NowVariable.java
index ab021979..97953460 100644
--- a/src/main/java/build/buf/protovalidate/NowVariable.java
+++ b/src/main/java/build/buf/protovalidate/NowVariable.java
@@ -15,7 +15,7 @@
 package build.buf.protovalidate;
 
 import java.time.Instant;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.common.types.TimestampT;
 import org.projectnessie.cel.interpreter.Activation;
 import org.projectnessie.cel.interpreter.ResolvedValue;
diff --git a/src/main/java/build/buf/protovalidate/ObjectValue.java b/src/main/java/build/buf/protovalidate/ObjectValue.java
index 1c897220..dcd2fa9b 100644
--- a/src/main/java/build/buf/protovalidate/ObjectValue.java
+++ b/src/main/java/build/buf/protovalidate/ObjectValue.java
@@ -22,7 +22,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.common.ULong;
 
 /** The {@link Value} type that contains a field descriptor and its value. */
diff --git a/src/main/java/build/buf/protovalidate/Value.java b/src/main/java/build/buf/protovalidate/Value.java
index 5aa920f2..3d9ae6a7 100644
--- a/src/main/java/build/buf/protovalidate/Value.java
+++ b/src/main/java/build/buf/protovalidate/Value.java
@@ -18,7 +18,7 @@
 import com.google.protobuf.Message;
 import java.util.List;
 import java.util.Map;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@link Value} is a wrapper around a protobuf value that provides helper methods for accessing the
@@ -31,8 +31,7 @@ interface Value {
    * @return The underlying {@link Descriptors.FieldDescriptor}. null if the underlying value is not
    *     a message field.
    */
-  @Nullable
-  Descriptors.FieldDescriptor fieldDescriptor();
+  Descriptors.@Nullable FieldDescriptor fieldDescriptor();
 
   /**
    * Get the underlying value as a {@link Message} type.
@@ -40,8 +39,7 @@ interface Value {
    * @return The underlying {@link Message} value. null if the underlying value is not a {@link
    *     Message} type.
    */
-  @Nullable
-  Message messageValue();
+  @Nullable Message messageValue();
 
   /**
    * Get the underlying value and cast it to the class type.
diff --git a/src/main/java/build/buf/protovalidate/ValueEvaluator.java b/src/main/java/build/buf/protovalidate/ValueEvaluator.java
index 8b0c0035..44af2d91 100644
--- a/src/main/java/build/buf/protovalidate/ValueEvaluator.java
+++ b/src/main/java/build/buf/protovalidate/ValueEvaluator.java
@@ -20,7 +20,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@link ValueEvaluator} performs validation on any concrete value contained within a singular
@@ -28,7 +28,7 @@
  */
 class ValueEvaluator implements Evaluator {
   /** The {@link Descriptors.FieldDescriptor} targeted by this evaluator */
-  @Nullable private final Descriptors.FieldDescriptor descriptor;
+  private final Descriptors.@Nullable FieldDescriptor descriptor;
 
   /** The nested rule path that this value evaluator is for */
   @Nullable private final FieldPath nestedRule;
@@ -46,12 +46,12 @@ class ValueEvaluator implements Evaluator {
   private boolean ignoreEmpty;
 
   /** Constructs a {@link ValueEvaluator}. */
-  ValueEvaluator(@Nullable Descriptors.FieldDescriptor descriptor, @Nullable FieldPath nestedRule) {
+  ValueEvaluator(Descriptors.@Nullable FieldDescriptor descriptor, @Nullable FieldPath nestedRule) {
     this.descriptor = descriptor;
     this.nestedRule = nestedRule;
   }
 
-  public @Nullable Descriptors.FieldDescriptor getDescriptor() {
+  public Descriptors.@Nullable FieldDescriptor getDescriptor() {
     return descriptor;
   }
 
diff --git a/src/main/java/build/buf/protovalidate/Variable.java b/src/main/java/build/buf/protovalidate/Variable.java
index e026ef6e..7f9830e3 100644
--- a/src/main/java/build/buf/protovalidate/Variable.java
+++ b/src/main/java/build/buf/protovalidate/Variable.java
@@ -14,7 +14,7 @@
 
 package build.buf.protovalidate;
 
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.projectnessie.cel.interpreter.Activation;
 import org.projectnessie.cel.interpreter.ResolvedValue;
 
diff --git a/src/main/java/build/buf/protovalidate/Violation.java b/src/main/java/build/buf/protovalidate/Violation.java
index b510ec00..ab91edaa 100644
--- a/src/main/java/build/buf/protovalidate/Violation.java
+++ b/src/main/java/build/buf/protovalidate/Violation.java
@@ -15,7 +15,7 @@
 package build.buf.protovalidate;
 
 import com.google.protobuf.Descriptors;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@link Violation} provides all of the collected information about an individual constraint
@@ -29,8 +29,7 @@ interface FieldValue {
      *
      * @return The value of the protobuf field.
      */
-    @Nullable
-    Object getValue();
+    @Nullable Object getValue();
 
     /**
      * Gets the field descriptor of the field this value is from.
@@ -52,14 +51,12 @@ interface FieldValue {
    *
    * @return Value of the field associated with the violation, or null if there is none.
    */
-  @Nullable
-  FieldValue getFieldValue();
+  @Nullable FieldValue getFieldValue();
 
   /**
    * Gets the value of the rule this violation pertains to, or null if there is none.
    *
    * @return Value of the rule associated with the violation, or null if there is none.
    */
-  @Nullable
-  FieldValue getRuleValue();
+  @Nullable FieldValue getRuleValue();
 }
diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java
index 24deb7ec..a4c7a4d1 100644
--- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java
+++ b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java
@@ -17,10 +17,8 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import java.util.List;
-import java.util.Map;
+import java.util.Arrays;
+import java.util.Collections;
 import org.junit.jupiter.api.Test;
 import org.projectnessie.cel.Ast;
 import org.projectnessie.cel.Env;
@@ -36,28 +34,20 @@ public class CustomOverloadTest {
 
   @Test
   public void testIsInf() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("0.0.isInf()", false)
-            .put("(1.0/0.0).isInf()", true)
-            .put("(1.0/0.0).isInf(0)", true)
-            .put("(1.0/0.0).isInf(1)", true)
-            .put("(1.0/0.0).isInf(-1)", false)
-            .put("(-1.0/0.0).isInf()", true)
-            .put("(-1.0/0.0).isInf(0)", true)
-            .put("(-1.0/0.0).isInf(1)", false)
-            .put("(-1.0/0.0).isInf(-1)", true)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue()).isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("0.0.isInf()")).isFalse();
+    assertThat(evalToBool("(1.0/0.0).isInf()")).isTrue();
+    assertThat(evalToBool("(1.0/0.0).isInf(0)")).isTrue();
+    assertThat(evalToBool("(1.0/0.0).isInf(1)")).isTrue();
+    assertThat(evalToBool("(1.0/0.0).isInf(-1)")).isFalse();
+    assertThat(evalToBool("(-1.0/0.0).isInf()")).isTrue();
+    assertThat(evalToBool("(-1.0/0.0).isInf(0)")).isTrue();
+    assertThat(evalToBool("(-1.0/0.0).isInf(1)")).isFalse();
+    assertThat(evalToBool("(-1.0/0.0).isInf(-1)")).isTrue();
   }
 
   @Test
   public void testIsInfUnsupported() {
-    List testCases = ImmutableList.of("'abc'.isInf()", "0.0.isInf('abc')");
-    for (String testCase : testCases) {
+    for (String testCase : Arrays.asList("'abc'.isInf()", "0.0.isInf('abc')")) {
       Val val = eval(testCase).getVal();
       assertThat(Err.isError(val)).isTrue();
       assertThatThrownBy(() -> val.convertToNative(Exception.class))
@@ -67,22 +57,14 @@ public void testIsInfUnsupported() {
 
   @Test
   public void testIsNan() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("0.0.isNan()", false)
-            .put("(0.0/0.0).isNan()", true)
-            .put("(1.0/0.0).isNan()", false)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue()).isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("0.0.isNan()")).isFalse();
+    assertThat(evalToBool("(0.0/0.0).isNan()")).isTrue();
+    assertThat(evalToBool("(1.0/0.0).isNan()")).isFalse();
   }
 
   @Test
   public void testIsNanUnsupported() {
-    List testCases = ImmutableList.of("'foo'.isNan()");
-    for (String testCase : testCases) {
+    for (String testCase : Collections.singletonList("'foo'.isNan()")) {
       Val val = eval(testCase).getVal();
       assertThat(Err.isError(val)).isTrue();
       assertThatThrownBy(() -> val.convertToNative(Exception.class))
@@ -92,37 +74,29 @@ public void testIsNanUnsupported() {
 
   @Test
   public void testUnique() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("[].unique()", true)
-            .put("[true].unique()", true)
-            .put("[true, false].unique()", true)
-            .put("[true, true].unique()", false)
-            .put("[1, 2, 3].unique()", true)
-            .put("[1, 2, 1].unique()", false)
-            .put("[1u, 2u, 3u].unique()", true)
-            .put("[1u, 2u, 2u].unique()", false)
-            .put("[1.0, 2.0, 3.0].unique()", true)
-            .put("[3.0,2.0,3.0].unique()", false)
-            .put("['abc', 'def'].unique()", true)
-            .put("['abc', 'abc'].unique()", false)
-            .put("[b'abc', b'123'].unique()", true)
-            .put("[b'123', b'123'].unique()", false)
-            // Previously, the unique() method returned false here as both bytes were converted
-            // to UTF-8. Since both contain invalid UTF-8, this would lead to them treated as equal
-            // because they'd have the same substitution character.
-            .put("[b'\\xFF', b'\\xFE'].unique()", true)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue()).isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("[].unique()")).isTrue();
+    assertThat(evalToBool("[true].unique()")).isTrue();
+    assertThat(evalToBool("[true, false].unique()")).isTrue();
+    assertThat(evalToBool("[true, true].unique()")).isFalse();
+    assertThat(evalToBool("[1, 2, 3].unique()")).isTrue();
+    assertThat(evalToBool("[1, 2, 1].unique()")).isFalse();
+    assertThat(evalToBool("[1u, 2u, 3u].unique()")).isTrue();
+    assertThat(evalToBool("[1u, 2u, 2u].unique()")).isFalse();
+    assertThat(evalToBool("[1.0, 2.0, 3.0].unique()")).isTrue();
+    assertThat(evalToBool("[3.0,2.0,3.0].unique()")).isFalse();
+    assertThat(evalToBool("['abc', 'def'].unique()")).isTrue();
+    assertThat(evalToBool("['abc', 'abc'].unique()")).isFalse();
+    assertThat(evalToBool("[b'abc', b'123'].unique()")).isTrue();
+    assertThat(evalToBool("[b'123', b'123'].unique()")).isFalse();
+    // Previously, the unique() method returned false here as both bytes were converted
+    // to UTF-8. Since both contain invalid UTF-8, this would lead to them treated as equal
+    // because they'd have the same substitution character.
+    assertThat(evalToBool("[b'\\xFF', b'\\xFE'].unique()")).isTrue();
   }
 
   @Test
   public void testUniqueUnsupported() {
-    List testCases = ImmutableList.of("1.unique()");
-    for (String testCase : testCases) {
+    for (String testCase : Collections.singletonList("1.unique()")) {
       Program.EvalResult result = eval(testCase);
       Val val = result.getVal();
       assertThat(Err.isError(val)).isTrue();
@@ -133,44 +107,40 @@ public void testUniqueUnsupported() {
 
   @Test
   public void testIsIpPrefix() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("'1.2.3.0/24'.isIpPrefix()", true)
-            .put("'1.2.3.4/24'.isIpPrefix()", true)
-            .put("'1.2.3.0/24'.isIpPrefix(true)", true)
-            .put("'1.2.3.4/24'.isIpPrefix(true)", false)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix()", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix()", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(true)", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(true)", false)
-            .put("'1.2.3.4'.isIpPrefix()", false)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b'.isIpPrefix()", false)
-            .put("'1.2.3.0/24'.isIpPrefix(4)", true)
-            .put("'1.2.3.4/24'.isIpPrefix(4)", true)
-            .put("'1.2.3.0/24'.isIpPrefix(4,true)", true)
-            .put("'1.2.3.4/24'.isIpPrefix(4,true)", false)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(4)", false)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6)", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6)", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6,true)", true)
-            .put("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6,true)", false)
-            .put("'1.2.3.0/24'.isIpPrefix(6)", false)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue()).isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix()")).isTrue();
+    assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix()")).isTrue();
+    assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(true)")).isTrue();
+    assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(true)")).isFalse();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix()")).isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix()")).isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(true)"))
+        .isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(true)"))
+        .isFalse();
+    assertThat(evalToBool("'1.2.3.4'.isIpPrefix()")).isFalse();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b'.isIpPrefix()")).isFalse();
+    assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(4)")).isTrue();
+    assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(4)")).isTrue();
+    assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(4,true)")).isTrue();
+    assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(4,true)")).isFalse();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(4)")).isFalse();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6)")).isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6)")).isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6,true)"))
+        .isTrue();
+    assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6,true)"))
+        .isFalse();
+    assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(6)")).isFalse();
   }
 
   @Test
   public void testIsIpPrefixUnsupported() {
-    List testCases =
-        ImmutableList.of(
+    for (String testCase :
+        Arrays.asList(
             "1.isIpPrefix()",
             "'1.2.3.0/24'.isIpPrefix('foo')",
             "'1.2.3.0/24'.isIpPrefix(4,'foo')",
-            "'1.2.3.0/24'.isIpPrefix('foo',true)");
-    for (String testCase : testCases) {
+            "'1.2.3.0/24'.isIpPrefix('foo',true)")) {
       Program.EvalResult result = eval(testCase);
       Val val = result.getVal();
       assertThat(Err.isError(val)).isTrue();
@@ -181,38 +151,32 @@ public void testIsIpPrefixUnsupported() {
 
   @Test
   public void testIsHostname() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("'example.com'.isHostname()", true)
-            .put("'example.123'.isHostname()", false)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue())
-          .as(
-              "expected %s=%s, got=%s",
-              testCase.getKey(), testCase.getValue(), !testCase.getValue())
-          .isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("'example.com'.isHostname()")).isTrue();
+    assertThat(evalToBool("'example.123'.isHostname()")).isFalse();
   }
 
   @Test
   public void testIsEmail() {
-    Map testCases =
-        ImmutableMap.builder()
-            .put("'foo@example.com'.isEmail()", true)
-            .put("''.isEmail()", false)
-            .put("'  foo@example.com'.isEmail()", false)
-            .put("'foo@example.com    '.isEmail()", false)
-            .build();
-    for (Map.Entry testCase : testCases.entrySet()) {
-      Program.EvalResult result = eval(testCase.getKey());
-      assertThat(result.getVal().booleanValue())
-          .as(
-              "expected %s=%s, got=%s",
-              testCase.getKey(), testCase.getValue(), !testCase.getValue())
-          .isEqualTo(testCase.getValue());
-    }
+    assertThat(evalToBool("'foo@example.com'.isEmail()")).isTrue();
+    assertThat(evalToBool("''.isEmail()")).isFalse();
+    assertThat(evalToBool("'  foo@example.com'.isEmail()")).isFalse();
+    assertThat(evalToBool("'foo@example.com    '.isEmail()")).isFalse();
+  }
+
+  @Test
+  public void testBytesContains() {
+    assertThat(evalToBool("bytes('12345').contains(bytes(''))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('1'))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('5'))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('123'))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('234'))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('345'))")).isTrue();
+    assertThat(evalToBool("bytes('12345').contains(bytes('12345'))")).isTrue();
+
+    assertThat(evalToBool("bytes('12345').contains(bytes('6'))")).isFalse();
+    assertThat(evalToBool("bytes('12345').contains(bytes('13'))")).isFalse();
+    assertThat(evalToBool("bytes('12345').contains(bytes('35'))")).isFalse();
+    assertThat(evalToBool("bytes('12345').contains(bytes('123456'))")).isFalse();
   }
 
   private Program.EvalResult eval(String source) {
@@ -225,4 +189,9 @@ private Program.EvalResult eval(String source, Object vars) {
     Ast ast = parsed.getAst();
     return env.program(ast).eval(vars);
   }
+
+  private boolean evalToBool(String source) {
+    Program.EvalResult result = eval(source);
+    return result.getVal().booleanValue();
+  }
 }

From 6a7192617bf23dc5288a65b336c0c8a95ae27d92 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 21 Apr 2025 11:05:51 -0700
Subject: [PATCH 08/12] Bump com.google.errorprone:error_prone_core from 2.37.0
 to 2.38.0 (#266)

Bumps
[com.google.errorprone:error_prone_core](https://github.com/google/error-prone)
from 2.37.0 to 2.38.0.
Release notes

Sourced from com.google.errorprone:error_prone_core's releases.

Error Prone 2.38.0

New checks:

Closed issues: #4924, #4897, #4995

Full changelog: https://github.com/google/error-prone/compare/v2.37.0...v2.38.0

Commits
  • a07bd3e Release Error Prone 2.38.0
  • 09fd394 Fix typo in NullTernary.md
  • 4171fd7 FindIdentifiers: find binding variables declared by enclosing or earlier if...
  • d78f515 Audit each use of ElementKind.LOCAL_VARIABLE, and add BINDING_VARIABLE if app...
  • 6f94a97 Tolerate default cases in switches as being present to handle version skew
  • 0223abb Support @LenientFormatString in LenientFormatStringValidation.
  • cb7dfaf Remove the Side enum.
  • d64c9ce Promote error prone check TestExceptionChecker to ERROR within Google (blaze ...
  • c0ce475 Move TargetType to a top-level class alongside ASTHelpers.
  • 90b8efb Allow binding to BINDING_VARIABLEs in GuardedByBinder.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.errorprone:error_prone_core&package-manager=gradle&previous-version=2.37.0&new-version=2.38.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- conformance/src/main/java/build/.DS_Store | Bin 0 -> 6148 bytes gradle/libs.versions.toml | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 conformance/src/main/java/build/.DS_Store diff --git a/conformance/src/main/java/build/.DS_Store b/conformance/src/main/java/build/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f17940234ba9b8c2694e83806d587a6e07ed9d52 GIT binary patch literal 6148 zcmeHKJ5Iwu5S<|@EFnZn%DqBvU?L+9kP9FaK1CwCN$-x5D{vZ0PQoF0^8p-Lq@;j1 z((Loj%-fY;;qi!wuJ)Uy$U;OaxS>4RGBwMaPwXTkbD-K8%k^!y>nC-`0>+(5D|@-$ zK8*SO`*&v7HBGziT12Yrv%BZ>x7W*Yc5@`#KaNj*0gXllr~nn90#x8%D}bJDR-FcN zr2tP_Vo|8!vS5ddhv@osqbSprxr0IU;-Ktx~~RA5jwM+^-*;w9_q z#33-~qJA^Z$(uDN6!qH?FJ3NM2XdtXRNzp7S!@^9|F7Ub%>RcZ?x+A2_)`k#yjeFZ zyi)eo$;(-a}{bi|zw C;U=R1 literal 0 HcmV?d00001 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21ac837c..6aaf45b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,10 @@ assertj = "3.27.3" buf = "1.52.1" cel = "0.5.1" -error-prone = "2.37.0" +error-prone = "2.38.0" junit = "5.12.1" maven-publish = "0.31.0" +jspecify = "1.0.0" # When updating, make sure to update versions in the following files to match and regenerate code with 'make generate'. # - buf.gen.yaml # - conformance/buf.gen.yaml @@ -19,7 +20,6 @@ cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" } cel-core = { module = "org.projectnessie.cel:cel-core" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" } errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" } -jspecify = { module ="org.jspecify:jspecify", version = "1.0.0" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.6" } From 2174ca35306afc93d969f24a8e57c6e62c4ba934 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:06:15 -0700 Subject: [PATCH 09/12] Bump net.ltgt.errorprone from 4.1.0 to 4.2.0 (#268) Bumps net.ltgt.errorprone from 4.1.0 to 4.2.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net.ltgt.errorprone&package-manager=gradle&previous-version=4.1.0&new-version=4.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6aaf45b0..17d558a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "p spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.0.2" } [plugins] -errorprone = { id = "net.ltgt.errorprone", version = "4.1.0" } +errorprone = { id = "net.ltgt.errorprone", version = "4.2.0" } git = { id = "com.palantir.git-version", version = "3.2.0" } maven = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish" } osdetector = { id = "com.google.osdetector", version = "1.7.3" } From 91ccc285040ff5877a65b7f131cc8fb5fff211ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:07:15 -0700 Subject: [PATCH 10/12] Bump org.junit:junit-bom from 5.12.1 to 5.12.2 (#270) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.12.1 to 5.12.2.
Release notes

Sourced from org.junit:junit-bom's releases.

JUnit 5.12.2 = Platform 1.12.2 + Jupiter 5.12.2 + Vintage 5.12.2

See Release Notes.

Full Changelog: https://github.com/junit-team/junit5/compare/r5.12.1...r5.12.2

Commits
  • 0a44659 Release 5.12.2
  • 4c7dfdc Finalize 5.12.2 release notes
  • 561613e Fix handling of CleanupMode.ON_SUCCESS
  • 19d07d2 Add 5.12.2 release notes from template
  • 803cbb6 Add build parameter for enabling dry-run mode for test execution
  • eb43e62 Back to snapshots for further development
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.junit:junit-bom&package-manager=gradle&previous-version=5.12.1&new-version=5.12.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17d558a6..e2eb88dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ assertj = "3.27.3" buf = "1.52.1" cel = "0.5.1" error-prone = "2.38.0" -junit = "5.12.1" +junit = "5.12.2" maven-publish = "0.31.0" jspecify = "1.0.0" # When updating, make sure to update versions in the following files to match and regenerate code with 'make generate'. From 2a862fb5cc1c160fb19876045040ca36c9284136 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:28:42 -0500 Subject: [PATCH 11/12] Bump com.diffplug.spotless:spotless-plugin-gradle from 7.0.2 to 7.0.3 (#269) Bumps [com.diffplug.spotless:spotless-plugin-gradle](https://github.com/diffplug/spotless) from 7.0.2 to 7.0.3.
Release notes

Sourced from com.diffplug.spotless:spotless-plugin-gradle's releases.

Gradle Plugin v7.0.3

Changed

  • Use palantir-java-format 2.57.0 on Java 21. (#2447)
  • Re-try npm install with --prefer-online after ERESOLVE error. (#2448)
  • Apply Gradle's strict plugin types validation to the Spotless plugin. (#2454)
Commits
  • 1b1a4fb Published gradle/7.0.3
  • 0fa3cab Published lib/3.1.1
  • 0fe8f9b Update README.md for android kotlin callouts (#2438)
  • d25f04d Minor tweak.
  • fa3fd1e Add the cool blockquote warning trick to the other Android spot.
  • 60993fd Fix the Android Kotlin warning and adjust its position.
  • 0426db1 Apply Gradle's strict plugin types validation to the Spotless plugin in prepa...
  • 48b4cde fix(deps): update dependency org.mockito:mockito-core to v5.17.0 (#2461)
  • 81f2f8b fix(deps): update dependency org.mockito:mockito-core to v5.17.0
  • fc0ec00 Update CHANGES.md for enabling Gradle's stricter plugin types validation
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.diffplug.spotless:spotless-plugin-gradle&package-manager=gradle&previous-version=7.0.2&new-version=7.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2eb88dc..34a4e30f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.6" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } -spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.0.2" } +spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.0.3" } [plugins] errorprone = { id = "net.ltgt.errorprone", version = "4.2.0" } From 492392b00378c9542f64cc17e63b54fad5b74624 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 22 Apr 2025 10:08:45 -0400 Subject: [PATCH 12/12] Merge --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34a4e30f..452f45f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,6 @@ cel = "0.5.1" error-prone = "2.38.0" junit = "5.12.2" maven-publish = "0.31.0" -jspecify = "1.0.0" # When updating, make sure to update versions in the following files to match and regenerate code with 'make generate'. # - buf.gen.yaml # - conformance/buf.gen.yaml @@ -20,6 +19,7 @@ cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" } cel-core = { module = "org.projectnessie.cel:cel-core" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" } errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" } +jspecify = { module ="org.jspecify:jspecify", version = "1.0.0" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.6" }