From 52f012bcfb83033c964ea45335d355103783ee31 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 10:57:13 -0400 Subject: [PATCH 01/17] Merge --- .vim/coc-settings.json | 3 + build.gradle.kts | 9 +- conformance/expected-failures.yaml | 88 -- .../buf/protovalidate/CustomOverload.java | 750 +++++++++++++++--- .../buf/protovalidate/CustomOverloadTest.java | 339 ++++---- 5 files changed, 826 insertions(+), 363 deletions(-) create mode 100644 .vim/coc-settings.json diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 00000000..0ca4d0be --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b53fb0c7..a71c0e13 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } // The releaseVersion property is set on official releases in the release.yml workflow. @@ -141,7 +141,7 @@ tasks.withType { dependsOn("generateTestSources") if (JavaVersion.current().isJava9Compatible) { doFirst { - options.compilerArgs = mutableListOf("--release", "8") + options.compilerArgs = mutableListOf("--release", "11") } } // Disable errorprone on generated code @@ -217,6 +217,9 @@ allprojects { } tasks.withType().configureEach { useJUnitPlatform() + this.testLogging { + this.showStandardStreams = true + } } } 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/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index 236df97c..fb91c749 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -14,20 +14,16 @@ 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.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; +import javax.annotation.Nullable; import org.projectnessie.cel.common.types.BoolT; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; @@ -74,15 +70,15 @@ static Overload[] create() { startsWith(), endsWith(), contains(), - isHostname(), - isEmail(), - isIp(), - isIpPrefix(), + celIsHostname(), + celIsEmail(), + celIsIp(), + celIsIpPrefix(), isUri(), isUriRef(), isNan(), isInf(), - isHostAndPort(), + celIsHostAndPort(), }; } @@ -226,7 +222,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 -> { @@ -237,7 +233,7 @@ private static Overload isHostname() { if (host.isEmpty()) { return BoolT.False; } - return Types.boolOf(validateHostname(host)); + return Types.boolOf(isHostname(host)); }); } @@ -246,7 +242,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 -> { @@ -257,7 +253,7 @@ private static Overload isEmail() { if (addr.isEmpty()) { return BoolT.False; } - return Types.boolOf(validateEmail(addr)); + return Types.boolOf(isEmail(addr)); }); } @@ -266,7 +262,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, @@ -278,7 +274,7 @@ private static Overload isIp() { 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) { @@ -288,7 +284,7 @@ private static Overload isIp() { if (address.isEmpty()) { return BoolT.False; } - return Types.boolOf(validateIP(address, rhs.intValue())); + return Types.boolOf(isIP(address, rhs.intValue())); }, null); } @@ -298,7 +294,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, @@ -311,7 +307,7 @@ private static Overload isIpPrefix() { 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 @@ -324,9 +320,9 @@ private static Overload isIpPrefix() { 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 @@ -339,8 +335,7 @@ private static Overload isIpPrefix() { 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())); }); } @@ -432,7 +427,7 @@ private static Overload isInf() { null); } - private static Overload isHostAndPort() { + private static Overload celIsHostAndPort() { return Overload.overload( OVERLOAD_IS_HOST_AND_PORT, null, @@ -443,40 +438,75 @@ 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: - An IPv4 address in dotted decimal format, for example + * "192.168.0.1". - An IPv6 address enclosed in square brackets, for example "[::1]". - A + * hostname, for example "example.com". + * + *

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.length() == 0) { 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)); + } else { // malformed + return false; } - 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) { + // isPort returns true if the string is a valid port for isHostAndPort. + private static boolean isPort(String str) { + if (str.length() == 0) { + 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) { + // Error converting to number return false; } } @@ -518,7 +548,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 +557,81 @@ 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: - The name consists of one or more labels, + * separated by a dot ("."). - Each label can be 1 to 63 alphanumeric characters. - A label can + * contain hyphens ("-"), but must not start or end with a hyphen. - The right-most label must not + * be digits only. - The name can have a trailing dot, for example "foo.example.com.". - The name + * can be 253 characters at most, excluding the optional trailing dot. */ - 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. * - * @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. + *

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". + * + *

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,40 +656,535 @@ 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. + * + *

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. + */ + public 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; + } +} + +final class Ipv4 { + private String str; + private int index; + private List octets; + private long prefixLen; + + Ipv4(String str) { + this.str = str; + this.octets = new ArrayList(); + } + + /** + + + 0000000000000000000000000000000010011000101100001001101010111001 + + 0000000000000000111111111111111111111111111111111111111111111111 + + */ + + + + /** + * Returns the 32-bit value of an address parsed through address() or addressPrefix(). + * + * Note Java does not support unsigned numeric types, so to handle unsigned + * 32-bit values, we need to use a 64-bit long type instead of the 32-bit (signed) Integer type. + * + *

Returns 0 if no address was parsed successfully. + */ + public 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. * - * @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. + *

Behavior is undefined if addressPrefix() has not been called before, or has returned false. */ - private static boolean validateIPPrefix(String prefix, long ver, boolean strict) { - IPAddressString str; - IPAddress addr; + public boolean isPrefixOnly() { + int bits = this.getBits(); + + int mask = 0; + if (this.prefixLen == 32) { + mask = 0xffffffff; + } else { + mask = ~(0xffffffff >>> this.prefixLen) >>> 0; + } + + int masked = (bits & mask) >>> 0; + + return bits == masked; + } + + // Parses an IPv4 Address in dotted decimal notation. + public boolean address() { + return this.addressPart() && this.index == this.str.length(); + } + + // Parses an IPv4 Address prefix. + public boolean addressPrefix() { + return this.addressPart() + && this.take('/') + && this.prefixLength() + && this.index == this.str.length(); + } + + private boolean prefixLength() { + int start = this.index; + + while (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + + 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.length() == 0) { + // too short + return false; + } + + if (str.length() > 1 && str.charAt(0) == '0') { + // bad leading 0 + return false; + } + try { - str = new IPAddressString(prefix); - addr = str.toAddress(); - } catch (Exception e) { + int val = Integer.parseInt(str); + + if (val > 32) { + // max 32 bits + return false; + } + + this.prefixLen = val; + + return true; + + } catch (NumberFormatException nfe) { + // Error converting to number return false; } - if (!addr.isPrefixed()) { + } + + 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 (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + + 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.length() == 0) { + // too short + return false; + } + + if (str.length() > 1 && str.charAt(0) == '0') { + // bad leading 0 return false; } - if (strict) { - IPAddress mask = addr.getNetworkMask().withoutPrefixLength(); - if (!addr.mask(mask).equals(str.getHostAddress())) { + + 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; } - if (ver == 0L) { + } + + private boolean digit() { + char c = this.str.charAt(this.index); + if ('0' <= c && c <= '9') { + this.index++; return true; - } else if (ver == 4L) { - return addr.isIPv4(); - } else if (ver == 6L) { - return addr.isIPv6(); } return false; } + + 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; + } +} + +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 long 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 (true) { + if (p16.size() >= 8) { + break; + } + // delete 0 entries at pos, insert a 0 + 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)) + }; + } + + public 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) >>> 0; + } + long masked = (p64 & mask) >>> 0; + if (p64 != masked) { + return false; + } + } + + return true; + } + + // Parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. + public boolean address() { + return this.addressPart() && this.index == this.str.length(); + } + + public boolean addressPrefix() { + return this.addressPart() + && !this.zoneIDFound + && this.take('/') + && this.prefixLength() + && this.index == this.str.length(); + } + + private boolean prefixLength() { + int start = this.index; + + while (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + + if (this.index - start > 3) { + return false; + } + } + + String str = this.str.substring(start, this.index); + + if (str.length() == 0) { + // 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; + } + } + + private boolean addressPart() { + while (true) { + if (this.index >= this.str.length()) { + break; + } + // 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; + } + + 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; + } + + private boolean dotted() { + int start = this.index; + + this.dottedRaw = ""; + + while (true) { + if (this.index < this.str.length() && (this.digit() || this.take('.'))) { + continue; + } + break; + } + + if (this.index - start >= 7) { + this.dottedRaw = this.str.substring(start, this.index); + + return true; + } + + this.index = start; + + return false; + } + + private boolean h16() { + int start = this.index; + + while (true) { + if (this.index >= this.str.length() || !this.hexDig()) { + break; + } + } + + String str = this.str.substring(start, this.index); + + if (str.length() == 0) { + // 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; + } + } + + 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; + } + + private boolean digit() { + char c = this.str.charAt(this.index); + if ('0' <= c && c <= '9') { + this.index++; + return true; + } + return false; + } + + 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/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java index 24deb7ec..ba8b8e5d 100644 --- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java +++ b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java @@ -18,9 +18,8 @@ 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 org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.projectnessie.cel.Ast; import org.projectnessie.cel.Env; @@ -36,25 +35,30 @@ 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()); - } + boolean b = CustomOverload.isIPPrefix("192.168.0.0/16", 4L, true); + assertThat(b).isTrue(); + // 127.0.0.1/16 + // 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()); + // } + } @Test + @Disabled("not today satan") public void testIsInfUnsupported() { List testCases = ImmutableList.of("'abc'.isInf()", "0.0.isInf('abc')"); for (String testCase : testCases) { @@ -65,155 +69,156 @@ 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()); - } - } - - @Test - public void testIsNanUnsupported() { - List testCases = ImmutableList.of("'foo'.isNan()"); - for (String testCase : testCases) { - Val val = eval(testCase).getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @Test - public void testUniqueUnsupported() { - List testCases = ImmutableList.of("1.unique()"); - for (String testCase : testCases) { - Program.EvalResult result = eval(testCase); - Val val = result.getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @Test - public void testIsIpPrefixUnsupported() { - List testCases = - ImmutableList.of( - "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) { - Program.EvalResult result = eval(testCase); - Val val = result.getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @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()); - } - } + // @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()); + // } + // } + + // @Test + // public void testIsNanUnsupported() { + // List testCases = ImmutableList.of("'foo'.isNan()"); + // for (String testCase : testCases) { + // Val val = eval(testCase).getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @Test + // public void testUniqueUnsupported() { + // List testCases = ImmutableList.of("1.unique()"); + // for (String testCase : testCases) { + // Program.EvalResult result = eval(testCase); + // Val val = result.getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @Test + // public void testIsIpPrefixUnsupported() { + // List testCases = + // ImmutableList.of( + // "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) { + // Program.EvalResult result = eval(testCase); + // Val val = result.getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @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()); + // } + // } private Program.EvalResult eval(String source) { return eval(source, Activation.emptyActivation()); From 2f0626931f94ff1b2f37f55c1560c5d79e7585a9 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 10:59:55 -0400 Subject: [PATCH 02/17] Fix --- .vim/coc-settings.json | 3 - .../buf/protovalidate/CustomOverload.java | 27 +- .../buf/protovalidate/CustomOverloadTest.java | 339 +++++++++--------- 3 files changed, 175 insertions(+), 194 deletions(-) delete mode 100644 .vim/coc-settings.json diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json deleted file mode 100644 index 0ca4d0be..00000000 --- a/.vim/coc-settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.compile.nullAnalysis.mode": "automatic" -} \ No newline at end of file diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index fb91c749..1f3a54a0 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -697,22 +697,11 @@ final class Ipv4 { this.octets = new ArrayList(); } - /** - - - 0000000000000000000000000000000010011000101100001001101010111001 - - 0000000000000000111111111111111111111111111111111111111111111111 - - */ - - - /** * Returns the 32-bit value of an address parsed through address() or addressPrefix(). - * - * Note Java does not support unsigned numeric types, so to handle unsigned - * 32-bit values, we need to use a 64-bit long type instead of the 32-bit (signed) Integer type. + * + *

Note Java does not support unsigned numeric types, so to handle unsigned 32-bit values, we + * need to use a 64-bit long type instead of the 32-bit (signed) Integer type. * *

Returns 0 if no address was parsed successfully. */ @@ -736,7 +725,7 @@ public boolean isPrefixOnly() { int mask = 0; if (this.prefixLen == 32) { - mask = 0xffffffff; + mask = 0xffffffff; } else { mask = ~(0xffffffff >>> this.prefixLen) >>> 0; } @@ -922,9 +911,9 @@ private long[] getBits() { // right-most 32 bits long dotted32 = this.dottedAddr.getBits(); // high 16 bits - p16.add((int)(dotted32 >> 16)); + p16.add((int) (dotted32 >> 16)); // low 16 bits - p16.add((int)dotted32); + p16.add((int) dotted32); } // handle double colon, fill pieces with 0 @@ -963,11 +952,11 @@ public boolean isPrefixOnly() { long mask = 0L; if (size >= 64) { - mask = 0xFFFFFFFFFFFFFFFFL; + mask = 0xFFFFFFFFFFFFFFFFL; } else if (size < 0) { mask = 0x0; } else { - mask = ~(0xFFFFFFFFFFFFFFFFL >>> size) >>> 0; + mask = ~(0xFFFFFFFFFFFFFFFFL >>> size) >>> 0; } long masked = (p64 & mask) >>> 0; if (p64 != masked) { diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java index ba8b8e5d..24deb7ec 100644 --- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java +++ b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java @@ -18,8 +18,9 @@ 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 org.junit.jupiter.api.Disabled; +import java.util.Map; import org.junit.jupiter.api.Test; import org.projectnessie.cel.Ast; import org.projectnessie.cel.Env; @@ -35,30 +36,25 @@ public class CustomOverloadTest { @Test public void testIsInf() { - boolean b = CustomOverload.isIPPrefix("192.168.0.0/16", 4L, true); - assertThat(b).isTrue(); - // 127.0.0.1/16 - // 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()); - // } - + 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()); + } } @Test - @Disabled("not today satan") public void testIsInfUnsupported() { List testCases = ImmutableList.of("'abc'.isInf()", "0.0.isInf('abc')"); for (String testCase : testCases) { @@ -69,156 +65,155 @@ 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()); - // } - // } - - // @Test - // public void testIsNanUnsupported() { - // List testCases = ImmutableList.of("'foo'.isNan()"); - // for (String testCase : testCases) { - // Val val = eval(testCase).getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @Test - // public void testUniqueUnsupported() { - // List testCases = ImmutableList.of("1.unique()"); - // for (String testCase : testCases) { - // Program.EvalResult result = eval(testCase); - // Val val = result.getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @Test - // public void testIsIpPrefixUnsupported() { - // List testCases = - // ImmutableList.of( - // "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) { - // Program.EvalResult result = eval(testCase); - // Val val = result.getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @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()); - // } - // } + @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()); + } + } + + @Test + public void testIsNanUnsupported() { + List testCases = ImmutableList.of("'foo'.isNan()"); + for (String testCase : testCases) { + Val val = eval(testCase).getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @Test + public void testUniqueUnsupported() { + List testCases = ImmutableList.of("1.unique()"); + for (String testCase : testCases) { + Program.EvalResult result = eval(testCase); + Val val = result.getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @Test + public void testIsIpPrefixUnsupported() { + List testCases = + ImmutableList.of( + "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) { + Program.EvalResult result = eval(testCase); + Val val = result.getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @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()); + } + } private Program.EvalResult eval(String source) { return eval(source, Activation.emptyActivation()); From 756f4a09caee7e45b568939622e1964fec116f06 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 11:02:35 -0400 Subject: [PATCH 03/17] Remove test logging --- build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a71c0e13..b681ddfb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -217,9 +217,6 @@ allprojects { } tasks.withType().configureEach { useJUnitPlatform() - this.testLogging { - this.showStandardStreams = true - } } } From 9cc805f6a554f86f66404a445b1199ea5d3320a6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 12:00:04 -0400 Subject: [PATCH 04/17] Cleanup --- build.gradle.kts | 1 - gradle/libs.versions.toml | 1 - .../buf/protovalidate/CustomOverload.java | 77 +++- .../buf/protovalidate/CustomOverloadTest.java | 339 +++++++++--------- 4 files changed, 240 insertions(+), 178 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b681ddfb..71014dcc 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 382e507b..a0fd94e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,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 1f3a54a0..a776f16e 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -486,7 +486,7 @@ private static boolean isHostAndPort(String str, boolean portRequired) { return ((isHostname(host) || isIP(host, 4)) && isPort(port)); } - // isPort returns true if the string is a valid port for isHostAndPort. + // Returns true if the string is a valid port for isHostAndPort. private static boolean isPort(String str) { if (str.length() == 0) { return false; @@ -672,7 +672,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. */ - public static boolean isIPPrefix(String str, long version, boolean strict) { + static boolean isIPPrefix(String str, long version, boolean strict) { if (version == 6L) { Ipv6 ip = new Ipv6(str); return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); @@ -705,7 +705,7 @@ final class Ipv4 { * *

Returns 0 if no address was parsed successfully. */ - public int getBits() { + int getBits() { if (this.octets.size() != 4) { return -1; } @@ -720,7 +720,7 @@ public int getBits() { * *

Behavior is undefined if addressPrefix() has not been called before, or has returned false. */ - public boolean isPrefixOnly() { + boolean isPrefixOnly() { int bits = this.getBits(); int mask = 0; @@ -736,18 +736,19 @@ public boolean isPrefixOnly() { } // Parses an IPv4 Address in dotted decimal notation. - public boolean address() { + boolean address() { return this.addressPart() && this.index == this.str.length(); } // Parses an IPv4 Address prefix. - public boolean addressPrefix() { + 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; @@ -851,6 +852,13 @@ private boolean decOctet() { } } + /** + * 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') { @@ -860,6 +868,11 @@ private boolean digit() { 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; @@ -943,7 +956,7 @@ private long[] getBits() { }; } - public boolean isPrefixOnly() { + 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++) { @@ -968,11 +981,12 @@ public boolean isPrefixOnly() { } // Parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. - public boolean address() { + boolean address() { return this.addressPart() && this.index == this.str.length(); } - public boolean addressPrefix() { + // Parse IPv6 Address Prefix following RFC 4291. Zone id is not permitted. + boolean addressPrefix() { return this.addressPart() && !this.zoneIDFound && this.take('/') @@ -980,6 +994,7 @@ public boolean addressPrefix() { && this.index == this.str.length(); } + // Stores value in `prefixLen` private boolean prefixLength() { int start = this.index; @@ -1023,6 +1038,7 @@ private boolean prefixLength() { } } + // Stores dotted notation for right-most 32 bits in `dottedRaw` / `dottedAddr` if found. private boolean addressPart() { while (true) { if (this.index >= this.str.length()) { @@ -1067,6 +1083,12 @@ private boolean addressPart() { 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; @@ -1086,6 +1108,15 @@ private boolean zoneID() { 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; @@ -1109,6 +1140,15 @@ private boolean dotted() { 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; @@ -1143,6 +1183,13 @@ private boolean h16() { } } + /** + * 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); @@ -1155,6 +1202,13 @@ private boolean hexDig() { 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') { @@ -1164,6 +1218,11 @@ private boolean digit() { 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; diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java index 24deb7ec..ba8b8e5d 100644 --- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java +++ b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java @@ -18,9 +18,8 @@ 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 org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.projectnessie.cel.Ast; import org.projectnessie.cel.Env; @@ -36,25 +35,30 @@ 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()); - } + boolean b = CustomOverload.isIPPrefix("192.168.0.0/16", 4L, true); + assertThat(b).isTrue(); + // 127.0.0.1/16 + // 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()); + // } + } @Test + @Disabled("not today satan") public void testIsInfUnsupported() { List testCases = ImmutableList.of("'abc'.isInf()", "0.0.isInf('abc')"); for (String testCase : testCases) { @@ -65,155 +69,156 @@ 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()); - } - } - - @Test - public void testIsNanUnsupported() { - List testCases = ImmutableList.of("'foo'.isNan()"); - for (String testCase : testCases) { - Val val = eval(testCase).getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @Test - public void testUniqueUnsupported() { - List testCases = ImmutableList.of("1.unique()"); - for (String testCase : testCases) { - Program.EvalResult result = eval(testCase); - Val val = result.getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @Test - public void testIsIpPrefixUnsupported() { - List testCases = - ImmutableList.of( - "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) { - Program.EvalResult result = eval(testCase); - Val val = result.getVal(); - assertThat(Err.isError(val)).isTrue(); - assertThatThrownBy(() -> val.convertToNative(Exception.class)) - .isInstanceOf(UnsupportedOperationException.class); - } - } - - @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()); - } - } - - @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()); - } - } + // @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()); + // } + // } + + // @Test + // public void testIsNanUnsupported() { + // List testCases = ImmutableList.of("'foo'.isNan()"); + // for (String testCase : testCases) { + // Val val = eval(testCase).getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @Test + // public void testUniqueUnsupported() { + // List testCases = ImmutableList.of("1.unique()"); + // for (String testCase : testCases) { + // Program.EvalResult result = eval(testCase); + // Val val = result.getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @Test + // public void testIsIpPrefixUnsupported() { + // List testCases = + // ImmutableList.of( + // "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) { + // Program.EvalResult result = eval(testCase); + // Val val = result.getVal(); + // assertThat(Err.isError(val)).isTrue(); + // assertThatThrownBy(() -> val.convertToNative(Exception.class)) + // .isInstanceOf(UnsupportedOperationException.class); + // } + // } + + // @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()); + // } + // } + + // @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()); + // } + // } private Program.EvalResult eval(String source) { return eval(source, Activation.emptyActivation()); From ce80f4cd266199b3c741234e3b99afee91379646 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 12:02:59 -0400 Subject: [PATCH 05/17] Revert --- .../buf/protovalidate/CustomOverloadTest.java | 339 +++++++++--------- 1 file changed, 167 insertions(+), 172 deletions(-) diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java index ba8b8e5d..24deb7ec 100644 --- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java +++ b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java @@ -18,8 +18,9 @@ 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 org.junit.jupiter.api.Disabled; +import java.util.Map; import org.junit.jupiter.api.Test; import org.projectnessie.cel.Ast; import org.projectnessie.cel.Env; @@ -35,30 +36,25 @@ public class CustomOverloadTest { @Test public void testIsInf() { - boolean b = CustomOverload.isIPPrefix("192.168.0.0/16", 4L, true); - assertThat(b).isTrue(); - // 127.0.0.1/16 - // 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()); - // } - + 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()); + } } @Test - @Disabled("not today satan") public void testIsInfUnsupported() { List testCases = ImmutableList.of("'abc'.isInf()", "0.0.isInf('abc')"); for (String testCase : testCases) { @@ -69,156 +65,155 @@ 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()); - // } - // } - - // @Test - // public void testIsNanUnsupported() { - // List testCases = ImmutableList.of("'foo'.isNan()"); - // for (String testCase : testCases) { - // Val val = eval(testCase).getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @Test - // public void testUniqueUnsupported() { - // List testCases = ImmutableList.of("1.unique()"); - // for (String testCase : testCases) { - // Program.EvalResult result = eval(testCase); - // Val val = result.getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @Test - // public void testIsIpPrefixUnsupported() { - // List testCases = - // ImmutableList.of( - // "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) { - // Program.EvalResult result = eval(testCase); - // Val val = result.getVal(); - // assertThat(Err.isError(val)).isTrue(); - // assertThatThrownBy(() -> val.convertToNative(Exception.class)) - // .isInstanceOf(UnsupportedOperationException.class); - // } - // } - - // @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()); - // } - // } - - // @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()); - // } - // } + @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()); + } + } + + @Test + public void testIsNanUnsupported() { + List testCases = ImmutableList.of("'foo'.isNan()"); + for (String testCase : testCases) { + Val val = eval(testCase).getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @Test + public void testUniqueUnsupported() { + List testCases = ImmutableList.of("1.unique()"); + for (String testCase : testCases) { + Program.EvalResult result = eval(testCase); + Val val = result.getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @Test + public void testIsIpPrefixUnsupported() { + List testCases = + ImmutableList.of( + "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) { + Program.EvalResult result = eval(testCase); + Val val = result.getVal(); + assertThat(Err.isError(val)).isTrue(); + assertThatThrownBy(() -> val.convertToNative(Exception.class)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + + @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()); + } + } + + @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()); + } + } private Program.EvalResult eval(String source) { return eval(source, Activation.emptyActivation()); From 69b15dcb95c332268cde96ec79eea4e73eba568b Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 14:40:21 -0400 Subject: [PATCH 06/17] Revert back to JDK 8 --- build.gradle.kts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 71014dcc..b53fb0c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } // The releaseVersion property is set on official releases in the release.yml workflow. @@ -141,7 +141,7 @@ tasks.withType { dependsOn("generateTestSources") if (JavaVersion.current().isJava9Compatible) { doFirst { - options.compilerArgs = mutableListOf("--release", "11") + options.compilerArgs = mutableListOf("--release", "8") } } // Disable errorprone on generated code @@ -271,6 +271,7 @@ 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") From 96666306af154c091d25ce0622385527b0292b14 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 14:41:41 -0400 Subject: [PATCH 07/17] Remove ip address --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) 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") From edae74fe30285c97e83c22443719fbac2238d24d Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 14:44:19 -0400 Subject: [PATCH 08/17] Remove ip address --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0fd94e4..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'. From 7c1deef2d8aad461f60f0ea2687cb0aaef01ae60 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 13:03:52 -0400 Subject: [PATCH 09/17] Feedback --- .../buf/protovalidate/CustomOverload.java | 151 +++++++----------- 1 file changed, 57 insertions(+), 94 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index a776f16e..8cce118c 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.ArrayList; import java.util.HashSet; import java.util.List; @@ -74,8 +72,8 @@ static Overload[] create() { celIsEmail(), celIsIp(), celIsIpPrefix(), - isUri(), - isUriRef(), + celIsUri(), + celIsUriRef(), isNan(), isInf(), celIsHostAndPort(), @@ -230,9 +228,6 @@ private static Overload celIsHostname() { return Err.noSuchOverload(value, OVERLOAD_IS_HOSTNAME, null); } String host = (String) value.value(); - if (host.isEmpty()) { - return BoolT.False; - } return Types.boolOf(isHostname(host)); }); } @@ -250,9 +245,6 @@ private static Overload celIsEmail() { return Err.noSuchOverload(value, OVERLOAD_IS_EMAIL, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } return Types.boolOf(isEmail(addr)); }); } @@ -271,9 +263,6 @@ private static Overload celIsIp() { return Err.noSuchOverload(value, OVERLOAD_IS_IP, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } return Types.boolOf(isIP(addr, 0L)); }, (lhs, rhs) -> { @@ -281,9 +270,6 @@ private static Overload celIsIp() { return Err.noSuchOverload(lhs, OVERLOAD_IS_IP, rhs); } String address = (String) lhs.value(); - if (address.isEmpty()) { - return BoolT.False; - } return Types.boolOf(isIP(address, rhs.intValue())); }, null); @@ -304,9 +290,6 @@ private static Overload celIsIpPrefix() { return Err.noSuchOverload(value, OVERLOAD_IS_IP_PREFIX, null); } String prefix = (String) value.value(); - if (prefix.isEmpty()) { - return BoolT.False; - } return Types.boolOf(isIPPrefix(prefix, 0L, false)); }, (lhs, rhs) -> { @@ -316,9 +299,6 @@ private static Overload celIsIpPrefix() { 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(isIPPrefix(prefix, rhs.intValue(), false)); } @@ -332,9 +312,6 @@ private static Overload celIsIpPrefix() { 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(isIPPrefix(prefix, values[1].intValue(), values[2].booleanValue())); }); } @@ -344,7 +321,7 @@ private static Overload celIsIpPrefix() { * * @return The {@link Overload} instance for the "isUri" operation. */ - private static Overload isUri() { + private static Overload celIsUri() { return Overload.unary( OVERLOAD_IS_URI, value -> { @@ -352,10 +329,7 @@ private static Overload isUri() { 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)); }); } @@ -364,7 +338,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 -> { @@ -372,10 +346,7 @@ private static Overload isUriRef() { 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)); }); } @@ -449,15 +420,19 @@ private static Overload celIsHostAndPort() { *

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: - An IPv4 address in dotted decimal format, for example - * "192.168.0.1". - An IPv6 address enclosed in square brackets, for example "[::1]". - A - * hostname, for example "example.com". + *

The host can be one of: + * + *

    + *
  • An IPv4 address in dotted decimal format, for example "192.168.0.1". + *
  • An IPv6 address enclosed in square brackets, for example "[::1]". + *
  • A hostname, for example "example.com". + *
* *

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.length() == 0) { + if (str.isEmpty()) { return false; } @@ -471,9 +446,8 @@ private static boolean isHostAndPort(String str, boolean portRequired) { 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)); - } else { // malformed - return false; } + return false; // malformed } if (splitIdx < 0) { @@ -488,7 +462,7 @@ private static boolean isHostAndPort(String str, boolean portRequired) { // Returns true if the string is a valid port for isHostAndPort. private static boolean isPort(String str) { - if (str.length() == 0) { + if (str.isEmpty()) { return false; } @@ -502,11 +476,8 @@ private static boolean isPort(String str) { try { int val = Integer.parseInt(str); - return val <= 65535; - } catch (NumberFormatException nfe) { - // Error converting to number return false; } } @@ -564,11 +535,16 @@ private static boolean isEmail(String addr) { /** * Returns true if the string is a valid hostname, for example "foo.example.com". * - *

A valid hostname follows the rules below: - The name consists of one or more labels, - * separated by a dot ("."). - Each label can be 1 to 63 alphanumeric characters. - A label can - * contain hyphens ("-"), but must not start or end with a hyphen. - The right-most label must not - * be digits only. - The name can have a trailing dot, for example "foo.example.com.". - The name - * can be 253 characters at most, excluding the optional trailing dot. + *

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

    + *
  • The name consists of one or more labels, separated by a dot ("."). + *
  • Each label can be 1 to 63 alphanumeric characters. + *
  • A label can contain hyphens ("-"), but must not start or end with a hyphen. + *
  • The right-most label must not be digits only. + *
  • The name can have a trailing dot, for example "foo.example.com.". + *
  • The name can be 253 characters at most, excluding the optional trailing dot. + *
*/ private static boolean isHostname(String val) { if (val.length() > 253) { @@ -625,7 +601,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) { @@ -637,22 +613,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; - } + public 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(); } /** @@ -672,7 +650,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. */ - 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()); @@ -700,10 +678,7 @@ final class Ipv4 { /** * Returns the 32-bit value of an address parsed through address() or addressPrefix(). * - *

Note Java does not support unsigned numeric types, so to handle unsigned 32-bit values, we - * need to use a 64-bit long type instead of the 32-bit (signed) Integer type. - * - *

Returns 0 if no address was parsed successfully. + *

Returns -1 if no address was parsed successfully. */ int getBits() { if (this.octets.size() != 4) { @@ -764,7 +739,7 @@ private boolean prefixLength() { } String str = this.str.substring(start, this.index); - if (str.length() == 0) { + if (str.isEmpty()) { // too short return false; } @@ -783,11 +758,8 @@ private boolean prefixLength() { } this.prefixLen = val; - return true; - } catch (NumberFormatException nfe) { - // Error converting to number return false; } } @@ -825,7 +797,7 @@ private boolean decOctet() { } String str = this.str.substring(start, this.index); - if (str.length() == 0) { + if (str.isEmpty()) { // too short return false; } @@ -845,7 +817,6 @@ private boolean decOctet() { this.octets.add((short) val); return true; - } catch (NumberFormatException nfe) { // Error converting to number return false; @@ -857,7 +828,7 @@ private boolean decOctet() { * *

Method parses the rule: * - *

DIGIT = %x30-39 ; 0-9 + *

DIGIT = %x30-39 ; 0-9
    */
   private boolean digit() {
     char c = this.str.charAt(this.index);
@@ -900,7 +871,7 @@ final class Ipv6 {
   // dotted notation successfully parsed as IPv4
   @Nullable private Ipv4 dottedAddr;
   private boolean zoneIDFound;
-  // 0 -128
+  // 0 - 128
   private long prefixLen;
 
   Ipv6(String str) {
@@ -998,11 +969,7 @@ boolean addressPrefix() {
   private boolean prefixLength() {
     int start = this.index;
 
-    while (true) {
-      if (this.index >= this.str.length() || !this.digit()) {
-        break;
-      }
-
+    while (this.index >= this.str.length() || !this.digit()) {
       if (this.index - start > 3) {
         return false;
       }
@@ -1010,7 +977,7 @@ private boolean prefixLength() {
 
     String str = this.str.substring(start, this.index);
 
-    if (str.length() == 0) {
+    if (str.isEmpty()) {
       // too short
       return false;
     }
@@ -1029,9 +996,7 @@ private boolean prefixLength() {
       }
 
       this.prefixLen = val;
-
       return true;
-
     } catch (NumberFormatException nfe) {
       // Error converting to number
       return false;
@@ -1087,7 +1052,7 @@ private boolean addressPart() {
    * 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 ) + *

RFC 6874: ZoneID = 1*( unreserved / pct-encoded )
    */
   private boolean zoneID() {
     int start = this.index;
@@ -1113,7 +1078,7 @@ private boolean zoneID() {
    *
    * 

Method parses the rule: * - *

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

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

Stores match in dottedRaw. */ @@ -1145,7 +1110,7 @@ private boolean dotted() { * *

Method parses the rule: * - *

h16 = 1*4HEXDIG + *

h16 = 1*4HEXDIG
    *
    * 

Stores 16-bit value in pieces. */ @@ -1160,7 +1125,7 @@ private boolean h16() { String str = this.str.substring(start, this.index); - if (str.length() == 0) { + if (str.isEmpty()) { // too short return false; } @@ -1174,9 +1139,7 @@ private boolean h16() { int val = Integer.parseInt(str, 16); this.pieces.add(val); - return true; - } catch (NumberFormatException nfe) { // Error converting to number return false; @@ -1188,7 +1151,7 @@ private boolean h16() { * *

Method parses the rule: * - *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
    */
   private boolean hexDig() {
     char c = this.str.charAt(this.index);
@@ -1207,7 +1170,7 @@ private boolean hexDig() {
    *
    * 

Method parses the rule: * - *

DIGIT = %x30-39 ; 0-9 + *

DIGIT = %x30-39 ; 0-9
    */
   private boolean digit() {
     char c = this.str.charAt(this.index);

From 847a663255103c9ae6b52c9efc4ec5526f552076 Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Wed, 2 Apr 2025 13:06:31 -0400
Subject: [PATCH 10/17] URI

---
 src/main/java/build/buf/protovalidate/CustomOverload.java | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java
index 8cce118c..b5bcca8e 100644
--- a/src/main/java/build/buf/protovalidate/CustomOverload.java
+++ b/src/main/java/build/buf/protovalidate/CustomOverload.java
@@ -15,6 +15,8 @@
 package build.buf.protovalidate;
 
 import com.google.common.primitives.Bytes;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;

From 2cda9db5757e8d4e331220238abdcafb6748a6c6 Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Wed, 2 Apr 2025 13:07:10 -0400
Subject: [PATCH 11/17] Fix

---
 .../buf/protovalidate/CustomOverload.java     | 30 +++++++++----------
 1 file changed, 14 insertions(+), 16 deletions(-)

diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java
index b5bcca8e..bdfd3226 100644
--- a/src/main/java/build/buf/protovalidate/CustomOverload.java
+++ b/src/main/java/build/buf/protovalidate/CustomOverload.java
@@ -615,24 +615,22 @@ static boolean isIP(String addr, long ver) {
   }
 
   /**
-   * Returns true if the string is a URI, for example "https://example.com/foo/bar?baz=quux#frag".
+   * Validates if the input string is a valid URI, which can be a URL or a URN.
    *
-   * 

URI is defined in the internet standard RFC 3986. Zone Identifiers in IPv6 address literals - * are supported (RFC 6874). + * @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. */ - public 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(); + 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; + } } /** From bd1628c5f2cc83013a50211345e0e360b44ef790 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 13:08:03 -0400 Subject: [PATCH 12/17] Fix --- src/main/java/build/buf/protovalidate/CustomOverload.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index bdfd3226..e0d8857c 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -331,7 +331,7 @@ private static Overload celIsUri() { return Err.noSuchOverload(value, OVERLOAD_IS_URI, null); } String addr = (String) value.value(); - return Types.boolOf(isURI(addr)); + return Types.boolOf(validateURI(addr, true)); }); } @@ -348,7 +348,7 @@ private static Overload celIsUriRef() { return Err.noSuchOverload(value, OVERLOAD_IS_URI_REF, null); } String addr = (String) value.value(); - return Types.boolOf(isURIRef(addr)); + return Types.boolOf(validateURIRef(addr, false)); }); } From 80d416844136e7adb2902d2245ab88c3730904a8 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 13:17:49 -0400 Subject: [PATCH 13/17] Feedback --- .../java/build/buf/protovalidate/CustomOverload.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index e0d8857c..a34033e9 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -348,7 +348,7 @@ private static Overload celIsUriRef() { return Err.noSuchOverload(value, OVERLOAD_IS_URI_REF, null); } String addr = (String) value.value(); - return Types.boolOf(validateURIRef(addr, false)); + return Types.boolOf(validateURI(addr, false)); }); } @@ -668,7 +668,7 @@ final class Ipv4 { private String str; private int index; private List octets; - private long prefixLen; + private int prefixLen; Ipv4(String str) { this.str = str; @@ -872,7 +872,7 @@ final class Ipv6 { @Nullable private Ipv4 dottedAddr; private boolean zoneIDFound; // 0 - 128 - private long prefixLen; + private int prefixLen; Ipv6(String str) { this.str = str; @@ -969,7 +969,10 @@ boolean addressPrefix() { private boolean prefixLength() { int start = this.index; - while (this.index >= this.str.length() || !this.digit()) { + while (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } if (this.index - start > 3) { return false; } From 0f7a79a315dbe82caf36a256954f2e3c84ee845d Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 16:03:23 -0400 Subject: [PATCH 14/17] Updates --- build.gradle.kts | 9 +- conformance/src/main/java/build/.DS_Store | Bin 0 -> 6148 bytes .../buf/protovalidate/CustomOverload.java | 544 +----------------- .../java/build/buf/protovalidate/Ipv4.java | 212 +++++++ .../java/build/buf/protovalidate/Ipv6.java | 363 ++++++++++++ 5 files changed, 582 insertions(+), 546 deletions(-) create mode 100644 conformance/src/main/java/build/.DS_Store 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 2bc2fa2c..490b36f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } // The releaseVersion property is set on official releases in the release.yml workflow. @@ -141,7 +141,7 @@ tasks.withType { dependsOn("generateTestSources") if (JavaVersion.current().isJava9Compatible) { doFirst { - options.compilerArgs = mutableListOf("--release", "8") + options.compilerArgs = mutableListOf("--release", "11") } } // Disable errorprone on generated code @@ -217,6 +217,9 @@ allprojects { } tasks.withType().configureEach { useJUnitPlatform() + this.testLogging { + this.showStandardStreams = true + } } } diff --git a/conformance/src/main/java/build/.DS_Store b/conformance/src/main/java/build/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5a89607bba7b4da317ed168692fbb6e517b9c701 GIT binary patch literal 6148 zcmeHKJ5Iwu5S<|@EFqK>6qI{~+`vRe9KaVqCVYwnr%CUQk}FYBawgt<07ntfQNSB% z_IYRK?aHt4ctk{(yVXKuE+Q4&P#$eLG|QVG*ilBNK(#X#?RCG|T2xOn_Y)X*BAx8y zc6~SIKg>Qeo4#qfb>AUUU7p-L&0b&5$Jy04+3o%4)b<%P8Wo@dRDcRlfq$(4dbU}0 z9LSXlPys6Np@4lK3f!CEfPXRn?~}Nr0#x8nDWKD) zZI*bY?5zhcXT7$-ui)>7S}(`qtr+O77#nNFXQR4e&&bz_L!i?UcRG+i0;UU%3jBrw EZzs(rVgLXD literal 0 HcmV?d00001 diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index a34033e9..8a9281f6 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -17,13 +17,10 @@ import com.google.common.primitives.Bytes; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; -import javax.annotation.Nullable; import org.projectnessie.cel.common.types.BoolT; import org.projectnessie.cel.common.types.Err; import org.projectnessie.cel.common.types.IntT; @@ -603,7 +600,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. */ - static boolean isIP(String addr, long ver) { + private static boolean isIP(String addr, long ver) { if (ver == 6L) { return new Ipv6(addr).address(); } else if (ver == 4L) { @@ -663,542 +660,3 @@ private static boolean isIPPrefix(String str, long version, boolean strict) { return false; } } - -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) >>> 0; - } - - int masked = (bits & mask) >>> 0; - - 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 (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } - - 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 (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } - - 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; - } -} - -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 (true) { - if (p16.size() >= 8) { - break; - } - // delete 0 entries at pos, insert a 0 - 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) >>> 0; - } - long masked = (p64 & mask) >>> 0; - 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 (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } - 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 (true) { - if (this.index >= this.str.length()) { - break; - } - // 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 (true) { - if (this.index < this.str.length() && (this.digit() || this.take('.'))) { - continue; - } - break; - } - - 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 (true) { - if (this.index >= this.str.length() || !this.hexDig()) { - break; - } - } - - 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; - } -} 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..5acd5cff --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -0,0 +1,212 @@ +// 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) >>> 0; + } + + int masked = (bits & mask) >>> 0; + + 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 (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + + 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 (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + + 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..df62bcfb --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Ipv6.java @@ -0,0 +1,363 @@ +// 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 (true) { + if (p16.size() >= 8) { + break; + } + 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) >>> 0; + } + long masked = (p64 & mask) >>> 0; + 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 (true) { + if (this.index >= this.str.length() || !this.digit()) { + break; + } + 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 (true) { + if (this.index >= this.str.length()) { + break; + } + // 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 (true) { + if (this.index < this.str.length() && (this.digit() || this.take('.'))) { + continue; + } + break; + } + + 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 (true) { + if (this.index >= this.str.length() || !this.hexDig()) { + break; + } + } + + 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 f050ad7467c74bdc8cd5f39c80b87700681de26a Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 16:09:17 -0400 Subject: [PATCH 15/17] Update --- build.gradle.kts | 9 +++------ conformance/src/main/java/build/.DS_Store | Bin 6148 -> 0 bytes .../build/buf/protovalidate/CustomOverload.java | 6 ++++++ 3 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 conformance/src/main/java/build/.DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index 490b36f7..2bc2fa2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } // The releaseVersion property is set on official releases in the release.yml workflow. @@ -141,7 +141,7 @@ tasks.withType { dependsOn("generateTestSources") if (JavaVersion.current().isJava9Compatible) { doFirst { - options.compilerArgs = mutableListOf("--release", "11") + options.compilerArgs = mutableListOf("--release", "8") } } // Disable errorprone on generated code @@ -217,9 +217,6 @@ allprojects { } tasks.withType().configureEach { useJUnitPlatform() - this.testLogging { - this.showStandardStreams = true - } } } diff --git a/conformance/src/main/java/build/.DS_Store b/conformance/src/main/java/build/.DS_Store deleted file mode 100644 index 5a89607bba7b4da317ed168692fbb6e517b9c701..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iwu5S<|@EFqK>6qI{~+`vRe9KaVqCVYwnr%CUQk}FYBawgt<07ntfQNSB% z_IYRK?aHt4ctk{(yVXKuE+Q4&P#$eLG|QVG*ilBNK(#X#?RCG|T2xOn_Y)X*BAx8y zc6~SIKg>Qeo4#qfb>AUUU7p-L&0b&5$Jy04+3o%4)b<%P8Wo@dRDcRlfq$(4dbU}0 z9LSXlPys6Np@4lK3f!CEfPXRn?~}Nr0#x8nDWKD) zZI*bY?5zhcXT7$-ui)>7S}(`qtr+O77#nNFXQR4e&&bz_L!i?UcRG+i0;UU%3jBrw EZzs(rVgLXD diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index 8a9281f6..889b0f78 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -328,6 +328,9 @@ 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)); }); } @@ -345,6 +348,9 @@ 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)); }); } From 01971b0f84f9984260d456ea308e04db110b9702 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 16:18:25 -0400 Subject: [PATCH 16/17] Cleanup --- .../java/build/buf/protovalidate/Ipv4.java | 12 ++------ .../java/build/buf/protovalidate/Ipv6.java | 28 ++++--------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java index 5acd5cff..8c9dc89c 100644 --- a/src/main/java/build/buf/protovalidate/Ipv4.java +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -80,11 +80,7 @@ boolean addressPrefix() { private boolean prefixLength() { int start = this.index; - while (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } - + 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; @@ -138,11 +134,7 @@ private boolean addressPart() { private boolean decOctet() { int start = this.index; - while (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } - + while (this.index < this.str.length() && this.digit()) { if (this.index - start > 3) { // decimal octet can be three characters at most return false; diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java index df62bcfb..78797b30 100644 --- a/src/main/java/build/buf/protovalidate/Ipv6.java +++ b/src/main/java/build/buf/protovalidate/Ipv6.java @@ -62,10 +62,7 @@ private long[] getBits() { // handle double colon, fill pieces with 0 if (this.doubleColonSeen) { - while (true) { - if (p16.size() >= 8) { - break; - } + while (p16.size() < 8) { p16.add(this.doubleColonAt, 0x00000000); } } @@ -128,10 +125,7 @@ boolean addressPrefix() { private boolean prefixLength() { int start = this.index; - while (true) { - if (this.index >= this.str.length() || !this.digit()) { - break; - } + while (this.index < this.str.length() && this.digit()) { if (this.index - start > 3) { return false; } @@ -167,10 +161,7 @@ private boolean prefixLength() { // Stores dotted notation for right-most 32 bits in `dottedRaw` / `dottedAddr` if found. private boolean addressPart() { - while (true) { - if (this.index >= this.str.length()) { - break; - } + 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); @@ -249,12 +240,7 @@ private boolean dotted() { this.dottedRaw = ""; - while (true) { - if (this.index < this.str.length() && (this.digit() || this.take('.'))) { - continue; - } - break; - } + while (this.index < this.str.length() && (this.digit() || this.take('.'))) {} if (this.index - start >= 7) { this.dottedRaw = this.str.substring(start, this.index); @@ -279,11 +265,7 @@ private boolean dotted() { private boolean h16() { int start = this.index; - while (true) { - if (this.index >= this.str.length() || !this.hexDig()) { - break; - } - } + while (this.index < this.str.length() && this.hexDig()) {} String str = this.str.substring(start, this.index); From 8ecbe585005f093470a4ef237d1315f1faeefc99 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 16:27:35 -0400 Subject: [PATCH 17/17] Remove unsigned shift to 0 --- src/main/java/build/buf/protovalidate/Ipv4.java | 4 ++-- src/main/java/build/buf/protovalidate/Ipv6.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java index 8c9dc89c..52f249a3 100644 --- a/src/main/java/build/buf/protovalidate/Ipv4.java +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -55,10 +55,10 @@ boolean isPrefixOnly() { if (this.prefixLen == 32) { mask = 0xffffffff; } else { - mask = ~(0xffffffff >>> this.prefixLen) >>> 0; + mask = ~(0xffffffff >>> this.prefixLen); } - int masked = (bits & mask) >>> 0; + int masked = bits & mask; return bits == masked; } diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java index 78797b30..aa178943 100644 --- a/src/main/java/build/buf/protovalidate/Ipv6.java +++ b/src/main/java/build/buf/protovalidate/Ipv6.java @@ -96,9 +96,9 @@ boolean isPrefixOnly() { } else if (size < 0) { mask = 0x0; } else { - mask = ~(0xFFFFFFFFFFFFFFFFL >>> size) >>> 0; + mask = ~(0xFFFFFFFFFFFFFFFFL >>> size); } - long masked = (p64 & mask) >>> 0; + long masked = p64 & mask; if (p64 != masked) { return false; }