From 52f012bcfb83033c964ea45335d355103783ee31 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Tue, 1 Apr 2025 10:57:13 -0400 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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 7499c31112ad7c1e39f6cdb7f669f7ff60cfabfe Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 12:47:50 -0400 Subject: [PATCH 09/26] URI --- build.gradle.kts | 9 +- .../buf/protovalidate/CustomOverload.java | 758 +++++++++++++++++- .../buf/protovalidate/CustomOverloadTest.java | 339 ++++---- 3 files changed, 911 insertions(+), 195 deletions(-) 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/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index a776f16e..15f240e4 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -15,8 +15,9 @@ package build.buf.protovalidate; import com.google.common.primitives.Bytes; -import java.net.URI; -import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -74,8 +75,8 @@ static Overload[] create() { celIsEmail(), celIsIp(), celIsIpPrefix(), - isUri(), - isUriRef(), + celIsUri(), + celIsUriRef(), isNan(), isInf(), celIsHostAndPort(), @@ -344,7 +345,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 -> { @@ -355,7 +356,7 @@ private static Overload isUri() { if (addr.isEmpty()) { return BoolT.False; } - return Types.boolOf(validateURI(addr, true)); + return Types.boolOf(isURI(addr)); }); } @@ -364,7 +365,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 -> { @@ -375,7 +376,7 @@ private static Overload isUriRef() { if (addr.isEmpty()) { return BoolT.False; } - return Types.boolOf(validateURI(addr, false)); + return Types.boolOf(isURIRef(addr)); }); } @@ -625,7 +626,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 +638,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 +675,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()); @@ -686,7 +689,7 @@ static boolean isIPPrefix(String str, long version, boolean strict) { } } -final class Ipv4 { +static final class Ipv4 { private String str; private int index; private List octets; @@ -1236,3 +1239,708 @@ private boolean take(char c) { return false; } } + +final class Uri { + private String str; + private int index; + private boolean pctEncodedFound; + + Uri(String str) { + this.str = str; + } + + /** + * Reports whether string is a valid URI. + * + *

Method parses the rule: + * + *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + */ + boolean uri() { + int start = this.index; + + if (!(this.scheme() && this.take(':') && this.hierPart())) { + this.index = start; + System.err.println("failed hier"); + return false; + } + + if (this.take('?') && !this.query()) { + System.err.println("failed cuir"); + return false; + } + + if (this.take('#') && !this.fragment()) { + System.err.println("failed fragomen"); + return false; + } + + if (this.index != this.str.length()) { + System.err.println("failed index"); + this.index = start; + return false; + } + + return true; + } + + /** + * Reports whether string contains a valid hier-part. + * + *

Method parses the rule: + * + *

hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty + */ + // The multiple take('/') invocations are intended. + @SuppressWarnings("IdentityBinaryExpression") + private boolean hierPart() { + int start = this.index; + + if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + System.err.println("trooby"); + return true; + } + + this.index = start; + + return this.pathAbsolute() || this.pathRootless() || this.pathEmpty(); + } + + /** + * Reports whether string is a valid URI reference. + * + *

Method parses the rule: + * + *

URI-reference = URI / relative-ref + */ + boolean uriReference() { + return this.uri() || this.relativeRef(); + } + + private boolean relativeRef() { + int start = this.index; + + if (!this.relativePart()) { + return false; + } + + if (this.take('?') && !this.query()) { + this.index = start; + return false; + } + + if (this.take('#') && !this.fragment()) { + this.index = start; + return false; + } + + if (this.index != this.str.length()) { + this.index = start; + return false; + } + + return true; + } + + // The multiple take('/') invocations are intended. + @SuppressWarnings("IdentityBinaryExpression") + private boolean relativePart() { + int start = this.index; + + if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + return true; + } + + this.index = start; + + return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty(); + } + + private boolean scheme() { + int start = this.index; + + if (this.alpha()) { + while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) { + // continue + } + + if (this.str.charAt(this.index) == ':') { + return true; + } + } + + this.index = start; + + return false; + } + + private boolean authority() { + int start = this.index; + + if (this.userinfo()) { + if (!this.take('@')) { + this.index = start; + System.err.println("auth1 fail"); + return false; + } + } + + if (!this.host()) { + this.index = start; + System.err.println("auth2 fail"); + return false; + } + + if (this.take(':')) { + if (!this.port()) { + this.index = start; + System.err.println("auth3 fail"); + return false; + } + } + + if (!this.isAuthorityEnd()) { + this.index = start; + System.err.println("auth4 fail"); + return false; + } + + return true; + } + + private boolean isAuthorityEnd() { + if (this.index >= this.str.length()) { + return true; + } + char c = this.str.charAt(this.index); + return (c == '?' || c == '#' || c == '/'); + } + + private boolean userinfo() { + int start = this.index; + + while (true) { + if (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take(':')) { + continue; + } + + if (this.index < this.str.length()) { + if (this.str.charAt(this.index) == '@') { + return true; + } + } + + this.index = start; + + return false; + } + } + + @FunctionalInterface + interface UnhexOperation { + int unhex(char c); + } + + // private boolean checkHostPctEncoded(String str) { + + // UnhexOperation fn = + // c -> { + // if ('0' <= c && c <= '9') { + // return c - '0'; + // } else if ('a' <= c && c <= 'f') { + // return c - 'a' + 10; + // } else if ('A' <= c && c <= 'F') { + // return c - 'A' + 10; + // } + + // return 0; + // }; + + // List escaped = new ArrayList(); + + // for (int i = 0; i < str.length(); ) { + // if (str.charAt(i) == '%') { + // escaped.add(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2))); + // i += 3; + // } else { + // escaped.add((int) str.charAt(i)); + // i++; + // } + // } + + // CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); + + // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input + // decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + // try { + // decoder.decode(java.nio.ByteBuffer.wrap(escaped)); // Attempt to decode + // return true; // No errors means valid UTF-8 + // } catch (Exception e) { + // return false; // Exception means invalid UTF-8 + // } + + // System.err.println(escaped); + + // return true; + // } + + private boolean host() { + if (this.index >= this.str.length()) { + return true; + } + + int start = this.index; + this.pctEncodedFound = false; + + // Note: IPv4address is a subset of reg-name + if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) { + if (this.pctEncodedFound) { + String rawHost = this.str.substring(start, this.index); + + if (!this.checkHostPctEncoded(rawHost)) { + return false; + } + // try { + // String s = URLDecoder.decode(rawHost, StandardCharsets.UTF_8.toString()); + // System.err.println(s); + // } catch (IllegalArgumentException e) { + // System.err.println("CRASH"); + // return false; + + // } catch (UnsupportedEncodingException e) { + // System.err.println("BOOM"); + // return false; + // } + } + + return true; + } + + return false; + } + + private boolean port() { + int start = this.index; + + while (true) { + if (this.digit()) { + continue; + } + + if (this.isAuthorityEnd()) { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean ipLiteral() { + int start = this.index; + + if (this.take('[')) { + int j = this.index; + + if (this.ipv6Address() && this.take(']')) { + return true; + } + + this.index = j; + + if (this.ipv6Addrz() && this.take(']')) { + return true; + } + + this.index = j; + + if (this.ipvFuture() && this.take(']')) { + return true; + } + } + + this.index = start; + + System.err.println("litch fail"); + return false; + } + + private boolean ipv6Address() { + int start = this.index; + + while (this.hexDig() || this.take(':')) { + // continue + } + + if (CustomOverload.isIP(this.str.substring(start, this.index), 6)) { + return true; + } + + this.index = start; + + return false; + } + + private boolean ipv6Addrz() { + int start = this.index; + + if (this.ipv6Address() && this.take('%') && this.take('2') && this.take('5') && this.zoneID()) { + return true; + } + + this.index = start; + + return false; + } + + private boolean zoneID() { + int start = this.index; + + while (this.unreserved() || this.pctEncoded()) { + // continue + } + + if (this.index - start > 0) { + return true; + } + + this.index = start; + + return false; + } + + private boolean ipvFuture() { + int start = this.index; + + if (this.take('v') && this.hexDig()) { + while (this.hexDig()) { + // continue; + } + + if (this.take('.')) { + int j = 0; + + while (this.unreserved() || this.subDelims() || this.take(':')) { + j++; + } + + if (j >= 1) { + return true; + } + } + } + + this.index = start; + + return false; + } + + private boolean regName() { + int start = this.index; + + while (true) { + if (this.unreserved() || this.pctEncoded() || this.subDelims()) { + continue; + } + + if (this.isAuthorityEnd()) { + // End of authority + return true; + } + + if (this.str.charAt(this.index) == ':') { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean isPathEnd() { + if (this.index >= this.str.length()) { + return true; + } + + char c = this.str.charAt(this.index); + + System.err.println("path ned fail"); + return (c == '?' || c == '#'); + } + + private boolean pathAbempty() { + int start = this.index; + + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + + this.index = start; + + return false; + } + + private boolean pathAbsolute() { + int start = this.index; + + if (this.take('/')) { + if (this.segmentNz()) { + while (this.take('/') && this.segment()) { + // continue + } + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + System.err.println("path abs fail"); + return false; + } + + private boolean pathNoscheme() { + int start = this.index; + + if (this.segmentNzNc()) { + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + return false; + } + + private boolean pathRootless() { + int start = this.index; + + if (this.segmentNz()) { + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + System.err.println("path root fail"); + return false; + } + + private boolean pathEmpty() { + return this.isPathEnd(); + } + + private boolean segment() { + while (this.pchar()) { + // continue + } + + return true; + } + + private boolean segmentNz() { + int start = this.index; + + if (this.pchar()) { + while (this.pchar()) { + // continue + } + return true; + } + + this.index = start; + + return false; + } + + private boolean segmentNzNc() { + int start = this.index; + + while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) { + // continue + } + + if (this.index - start > 0) { + return true; + } + + this.index = start; + + return false; + } + + private boolean pchar() { + return (this.unreserved() + || this.pctEncoded() + || this.subDelims() + || this.take(':') + || this.take('@')); + } + + private boolean query() { + int start = this.index; + + while (true) { + if (this.pchar() || this.take('/') || this.take('?')) { + continue; + } + + if (this.index == this.str.length() || this.str.charAt(this.index) == '#') { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean fragment() { + int start = this.index; + + while (true) { + if (this.pchar() || this.take('/') || this.take('?')) { + continue; + } + + if (this.index == this.str.length()) { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean pctEncoded() { + int start = this.index; + + if (this.take('%') && this.hexDig() && this.hexDig()) { + this.pctEncodedFound = true; + + return true; + } + + this.index = start; + + return false; + } + + private boolean unreserved() { + return (this.alpha() + || this.digit() + || this.take('-') + || this.take('_') + || this.take('.') + || this.take('~')); + } + + private boolean subDelims() { + return (this.take('!') + || this.take('$') + || this.take('&') + || this.take('\'') + || this.take('(') + || this.take(')') + || this.take('*') + || this.take('+') + || this.take(',') + || this.take(';') + || this.take('=')); + } + + private boolean alpha() { + if (this.index >= this.str.length()) { + return false; + } + + char c = this.str.charAt(this.index); + + if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { + this.index++; + return true; + } + + return false; + } + + /** + * Reports whether the current position is a hex digit. + * + *

Method parses the rule: + * + *

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

Method parses the rule: + * + *

DIGIT = %x30-39 ; 0-9 + */ + private boolean digit() { + if (this.index >= this.str.length()) { + return false; + } + + char c = this.str.charAt(this.index); + if ('0' <= c && c <= '9') { + this.index++; + return true; + } + return false; + } + + /** + * Take the given char at the current 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/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java index 24deb7ec..715f759b 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.isURI("https://[::1%25foo%c3x%96]"); + assertThat(b).isFalse(); + // 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 7c1deef2d8aad461f60f0ea2687cb0aaef01ae60 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 2 Apr 2025 13:03:52 -0400 Subject: [PATCH 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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; } From 28b0b752699dc64664b12536830550c5328fb3c8 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 3 Apr 2025 11:21:53 -0400 Subject: [PATCH 19/26] Fix --- conformance/src/main/java/build/.DS_Store | Bin 0 -> 6148 bytes .../buf/protovalidate/CustomOverload.java | 708 ----------------- .../java/build/buf/protovalidate/Uri.java | 720 ++++++++++++++++++ 3 files changed, 720 insertions(+), 708 deletions(-) create mode 100644 conformance/src/main/java/build/.DS_Store create mode 100644 src/main/java/build/buf/protovalidate/Uri.java 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 15f240e4..7388c8bd 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -15,9 +15,6 @@ package build.buf.protovalidate; import com.google.common.primitives.Bytes; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -1239,708 +1236,3 @@ private boolean take(char c) { return false; } } - -final class Uri { - private String str; - private int index; - private boolean pctEncodedFound; - - Uri(String str) { - this.str = str; - } - - /** - * Reports whether string is a valid URI. - * - *

Method parses the rule: - * - *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] - */ - boolean uri() { - int start = this.index; - - if (!(this.scheme() && this.take(':') && this.hierPart())) { - this.index = start; - System.err.println("failed hier"); - return false; - } - - if (this.take('?') && !this.query()) { - System.err.println("failed cuir"); - return false; - } - - if (this.take('#') && !this.fragment()) { - System.err.println("failed fragomen"); - return false; - } - - if (this.index != this.str.length()) { - System.err.println("failed index"); - this.index = start; - return false; - } - - return true; - } - - /** - * Reports whether string contains a valid hier-part. - * - *

Method parses the rule: - * - *

hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty - */ - // The multiple take('/') invocations are intended. - @SuppressWarnings("IdentityBinaryExpression") - private boolean hierPart() { - int start = this.index; - - if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { - System.err.println("trooby"); - return true; - } - - this.index = start; - - return this.pathAbsolute() || this.pathRootless() || this.pathEmpty(); - } - - /** - * Reports whether string is a valid URI reference. - * - *

Method parses the rule: - * - *

URI-reference = URI / relative-ref - */ - boolean uriReference() { - return this.uri() || this.relativeRef(); - } - - private boolean relativeRef() { - int start = this.index; - - if (!this.relativePart()) { - return false; - } - - if (this.take('?') && !this.query()) { - this.index = start; - return false; - } - - if (this.take('#') && !this.fragment()) { - this.index = start; - return false; - } - - if (this.index != this.str.length()) { - this.index = start; - return false; - } - - return true; - } - - // The multiple take('/') invocations are intended. - @SuppressWarnings("IdentityBinaryExpression") - private boolean relativePart() { - int start = this.index; - - if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { - return true; - } - - this.index = start; - - return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty(); - } - - private boolean scheme() { - int start = this.index; - - if (this.alpha()) { - while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) { - // continue - } - - if (this.str.charAt(this.index) == ':') { - return true; - } - } - - this.index = start; - - return false; - } - - private boolean authority() { - int start = this.index; - - if (this.userinfo()) { - if (!this.take('@')) { - this.index = start; - System.err.println("auth1 fail"); - return false; - } - } - - if (!this.host()) { - this.index = start; - System.err.println("auth2 fail"); - return false; - } - - if (this.take(':')) { - if (!this.port()) { - this.index = start; - System.err.println("auth3 fail"); - return false; - } - } - - if (!this.isAuthorityEnd()) { - this.index = start; - System.err.println("auth4 fail"); - return false; - } - - return true; - } - - private boolean isAuthorityEnd() { - if (this.index >= this.str.length()) { - return true; - } - char c = this.str.charAt(this.index); - return (c == '?' || c == '#' || c == '/'); - } - - private boolean userinfo() { - int start = this.index; - - while (true) { - if (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take(':')) { - continue; - } - - if (this.index < this.str.length()) { - if (this.str.charAt(this.index) == '@') { - return true; - } - } - - this.index = start; - - return false; - } - } - - @FunctionalInterface - interface UnhexOperation { - int unhex(char c); - } - - // private boolean checkHostPctEncoded(String str) { - - // UnhexOperation fn = - // c -> { - // if ('0' <= c && c <= '9') { - // return c - '0'; - // } else if ('a' <= c && c <= 'f') { - // return c - 'a' + 10; - // } else if ('A' <= c && c <= 'F') { - // return c - 'A' + 10; - // } - - // return 0; - // }; - - // List escaped = new ArrayList(); - - // for (int i = 0; i < str.length(); ) { - // if (str.charAt(i) == '%') { - // escaped.add(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2))); - // i += 3; - // } else { - // escaped.add((int) str.charAt(i)); - // i++; - // } - // } - - // CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); - - // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input - // decoder.onUnmappableCharacter(CodingErrorAction.REPORT); - - // try { - // decoder.decode(java.nio.ByteBuffer.wrap(escaped)); // Attempt to decode - // return true; // No errors means valid UTF-8 - // } catch (Exception e) { - // return false; // Exception means invalid UTF-8 - // } - - // System.err.println(escaped); - - // return true; - // } - - private boolean host() { - if (this.index >= this.str.length()) { - return true; - } - - int start = this.index; - this.pctEncodedFound = false; - - // Note: IPv4address is a subset of reg-name - if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) { - if (this.pctEncodedFound) { - String rawHost = this.str.substring(start, this.index); - - if (!this.checkHostPctEncoded(rawHost)) { - return false; - } - // try { - // String s = URLDecoder.decode(rawHost, StandardCharsets.UTF_8.toString()); - // System.err.println(s); - // } catch (IllegalArgumentException e) { - // System.err.println("CRASH"); - // return false; - - // } catch (UnsupportedEncodingException e) { - // System.err.println("BOOM"); - // return false; - // } - } - - return true; - } - - return false; - } - - private boolean port() { - int start = this.index; - - while (true) { - if (this.digit()) { - continue; - } - - if (this.isAuthorityEnd()) { - return true; - } - - this.index = start; - - return false; - } - } - - private boolean ipLiteral() { - int start = this.index; - - if (this.take('[')) { - int j = this.index; - - if (this.ipv6Address() && this.take(']')) { - return true; - } - - this.index = j; - - if (this.ipv6Addrz() && this.take(']')) { - return true; - } - - this.index = j; - - if (this.ipvFuture() && this.take(']')) { - return true; - } - } - - this.index = start; - - System.err.println("litch fail"); - return false; - } - - private boolean ipv6Address() { - int start = this.index; - - while (this.hexDig() || this.take(':')) { - // continue - } - - if (CustomOverload.isIP(this.str.substring(start, this.index), 6)) { - return true; - } - - this.index = start; - - return false; - } - - private boolean ipv6Addrz() { - int start = this.index; - - if (this.ipv6Address() && this.take('%') && this.take('2') && this.take('5') && this.zoneID()) { - return true; - } - - this.index = start; - - return false; - } - - private boolean zoneID() { - int start = this.index; - - while (this.unreserved() || this.pctEncoded()) { - // continue - } - - if (this.index - start > 0) { - return true; - } - - this.index = start; - - return false; - } - - private boolean ipvFuture() { - int start = this.index; - - if (this.take('v') && this.hexDig()) { - while (this.hexDig()) { - // continue; - } - - if (this.take('.')) { - int j = 0; - - while (this.unreserved() || this.subDelims() || this.take(':')) { - j++; - } - - if (j >= 1) { - return true; - } - } - } - - this.index = start; - - return false; - } - - private boolean regName() { - int start = this.index; - - while (true) { - if (this.unreserved() || this.pctEncoded() || this.subDelims()) { - continue; - } - - if (this.isAuthorityEnd()) { - // End of authority - return true; - } - - if (this.str.charAt(this.index) == ':') { - return true; - } - - this.index = start; - - return false; - } - } - - private boolean isPathEnd() { - if (this.index >= this.str.length()) { - return true; - } - - char c = this.str.charAt(this.index); - - System.err.println("path ned fail"); - return (c == '?' || c == '#'); - } - - private boolean pathAbempty() { - int start = this.index; - - while (this.take('/') && this.segment()) { - // continue - } - - if (this.isPathEnd()) { - return true; - } - - this.index = start; - - return false; - } - - private boolean pathAbsolute() { - int start = this.index; - - if (this.take('/')) { - if (this.segmentNz()) { - while (this.take('/') && this.segment()) { - // continue - } - } - - if (this.isPathEnd()) { - return true; - } - } - - this.index = start; - - System.err.println("path abs fail"); - return false; - } - - private boolean pathNoscheme() { - int start = this.index; - - if (this.segmentNzNc()) { - while (this.take('/') && this.segment()) { - // continue - } - - if (this.isPathEnd()) { - return true; - } - } - - this.index = start; - - return false; - } - - private boolean pathRootless() { - int start = this.index; - - if (this.segmentNz()) { - while (this.take('/') && this.segment()) { - // continue - } - - if (this.isPathEnd()) { - return true; - } - } - - this.index = start; - - System.err.println("path root fail"); - return false; - } - - private boolean pathEmpty() { - return this.isPathEnd(); - } - - private boolean segment() { - while (this.pchar()) { - // continue - } - - return true; - } - - private boolean segmentNz() { - int start = this.index; - - if (this.pchar()) { - while (this.pchar()) { - // continue - } - return true; - } - - this.index = start; - - return false; - } - - private boolean segmentNzNc() { - int start = this.index; - - while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) { - // continue - } - - if (this.index - start > 0) { - return true; - } - - this.index = start; - - return false; - } - - private boolean pchar() { - return (this.unreserved() - || this.pctEncoded() - || this.subDelims() - || this.take(':') - || this.take('@')); - } - - private boolean query() { - int start = this.index; - - while (true) { - if (this.pchar() || this.take('/') || this.take('?')) { - continue; - } - - if (this.index == this.str.length() || this.str.charAt(this.index) == '#') { - return true; - } - - this.index = start; - - return false; - } - } - - private boolean fragment() { - int start = this.index; - - while (true) { - if (this.pchar() || this.take('/') || this.take('?')) { - continue; - } - - if (this.index == this.str.length()) { - return true; - } - - this.index = start; - - return false; - } - } - - private boolean pctEncoded() { - int start = this.index; - - if (this.take('%') && this.hexDig() && this.hexDig()) { - this.pctEncodedFound = true; - - return true; - } - - this.index = start; - - return false; - } - - private boolean unreserved() { - return (this.alpha() - || this.digit() - || this.take('-') - || this.take('_') - || this.take('.') - || this.take('~')); - } - - private boolean subDelims() { - return (this.take('!') - || this.take('$') - || this.take('&') - || this.take('\'') - || this.take('(') - || this.take(')') - || this.take('*') - || this.take('+') - || this.take(',') - || this.take(';') - || this.take('=')); - } - - private boolean alpha() { - if (this.index >= this.str.length()) { - return false; - } - - char c = this.str.charAt(this.index); - - if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { - this.index++; - return true; - } - - return false; - } - - /** - * Reports whether the current position is a hex digit. - * - *

Method parses the rule: - * - *

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

Method parses the rule: - * - *

DIGIT = %x30-39 ; 0-9 - */ - private boolean digit() { - if (this.index >= this.str.length()) { - return false; - } - - char c = this.str.charAt(this.index); - if ('0' <= c && c <= '9') { - this.index++; - return true; - } - return false; - } - - /** - * Take the given char at the current 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/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java new file mode 100644 index 00000000..bc997df0 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -0,0 +1,720 @@ +// 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; + +final class Uri { + private String str; + private int index; + private boolean pctEncodedFound; + + Uri(String str) { + this.str = str; + } + + /** + * Reports whether string is a valid URI. + * + *

Method parses the rule: + * + *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + */ + boolean uri() { + int start = this.index; + + if (!(this.scheme() && this.take(':') && this.hierPart())) { + this.index = start; + System.err.println("failed hier"); + return false; + } + + if (this.take('?') && !this.query()) { + System.err.println("failed cuir"); + return false; + } + + if (this.take('#') && !this.fragment()) { + System.err.println("failed fragomen"); + return false; + } + + if (this.index != this.str.length()) { + System.err.println("failed index"); + this.index = start; + return false; + } + + return true; + } + + /** + * Reports whether string contains a valid hier-part. + * + *

Method parses the rule: + * + *

hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty + */ + // The multiple take('/') invocations are intended. + @SuppressWarnings("IdentityBinaryExpression") + private boolean hierPart() { + int start = this.index; + + if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + System.err.println("trooby"); + return true; + } + + this.index = start; + + return this.pathAbsolute() || this.pathRootless() || this.pathEmpty(); + } + + /** + * Reports whether string is a valid URI reference. + * + *

Method parses the rule: + * + *

URI-reference = URI / relative-ref + */ + boolean uriReference() { + return this.uri() || this.relativeRef(); + } + + private boolean relativeRef() { + int start = this.index; + + if (!this.relativePart()) { + return false; + } + + if (this.take('?') && !this.query()) { + this.index = start; + return false; + } + + if (this.take('#') && !this.fragment()) { + this.index = start; + return false; + } + + if (this.index != this.str.length()) { + this.index = start; + return false; + } + + return true; + } + + // The multiple take('/') invocations are intended. + @SuppressWarnings("IdentityBinaryExpression") + private boolean relativePart() { + int start = this.index; + + if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + return true; + } + + this.index = start; + + return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty(); + } + + private boolean scheme() { + int start = this.index; + + if (this.alpha()) { + while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) { + // continue + } + + if (this.str.charAt(this.index) == ':') { + return true; + } + } + + this.index = start; + + return false; + } + + private boolean authority() { + int start = this.index; + + if (this.userinfo()) { + if (!this.take('@')) { + this.index = start; + System.err.println("auth1 fail"); + return false; + } + } + + if (!this.host()) { + this.index = start; + System.err.println("auth2 fail"); + return false; + } + + if (this.take(':')) { + if (!this.port()) { + this.index = start; + System.err.println("auth3 fail"); + return false; + } + } + + if (!this.isAuthorityEnd()) { + this.index = start; + System.err.println("auth4 fail"); + return false; + } + + return true; + } + + private boolean isAuthorityEnd() { + if (this.index >= this.str.length()) { + return true; + } + char c = this.str.charAt(this.index); + return (c == '?' || c == '#' || c == '/'); + } + + private boolean userinfo() { + int start = this.index; + + while (true) { + if (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take(':')) { + continue; + } + + if (this.index < this.str.length()) { + if (this.str.charAt(this.index) == '@') { + return true; + } + } + + this.index = start; + + return false; + } + } + + @FunctionalInterface + interface UnhexOperation { + int unhex(char c); + } + + // private boolean checkHostPctEncoded(String str) { + + // UnhexOperation fn = + // c -> { + // if ('0' <= c && c <= '9') { + // return c - '0'; + // } else if ('a' <= c && c <= 'f') { + // return c - 'a' + 10; + // } else if ('A' <= c && c <= 'F') { + // return c - 'A' + 10; + // } + + // return 0; + // }; + + // List escaped = new ArrayList(); + + // for (int i = 0; i < str.length(); ) { + // if (str.charAt(i) == '%') { + // escaped.add(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2))); + // i += 3; + // } else { + // escaped.add((int) str.charAt(i)); + // i++; + // } + // } + + // CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); + + // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input + // decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + // try { + // decoder.decode(java.nio.ByteBuffer.wrap(escaped)); // Attempt to decode + // return true; // No errors means valid UTF-8 + // } catch (Exception e) { + // return false; // Exception means invalid UTF-8 + // } + + // System.err.println(escaped); + + // return true; + // } + + private boolean host() { + if (this.index >= this.str.length()) { + return true; + } + + int start = this.index; + this.pctEncodedFound = false; + + // Note: IPv4address is a subset of reg-name + if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) { + if (this.pctEncodedFound) { + String rawHost = this.str.substring(start, this.index); + + if (!this.checkHostPctEncoded(rawHost)) { + return false; + } + // try { + // String s = URLDecoder.decode(rawHost, StandardCharsets.UTF_8.toString()); + // System.err.println(s); + // } catch (IllegalArgumentException e) { + // System.err.println("CRASH"); + // return false; + + // } catch (UnsupportedEncodingException e) { + // System.err.println("BOOM"); + // return false; + // } + } + + return true; + } + + return false; + } + + private boolean port() { + int start = this.index; + + while (true) { + if (this.digit()) { + continue; + } + + if (this.isAuthorityEnd()) { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean ipLiteral() { + int start = this.index; + + if (this.take('[')) { + int j = this.index; + + if (this.ipv6Address() && this.take(']')) { + return true; + } + + this.index = j; + + if (this.ipv6Addrz() && this.take(']')) { + return true; + } + + this.index = j; + + if (this.ipvFuture() && this.take(']')) { + return true; + } + } + + this.index = start; + + System.err.println("litch fail"); + return false; + } + + private boolean ipv6Address() { + int start = this.index; + + while (this.hexDig() || this.take(':')) { + // continue + } + + if (CustomOverload.isIP(this.str.substring(start, this.index), 6)) { + return true; + } + + this.index = start; + + return false; + } + + private boolean ipv6Addrz() { + int start = this.index; + + if (this.ipv6Address() && this.take('%') && this.take('2') && this.take('5') && this.zoneID()) { + return true; + } + + this.index = start; + + return false; + } + + private boolean zoneID() { + int start = this.index; + + while (this.unreserved() || this.pctEncoded()) { + // continue + } + + if (this.index - start > 0) { + return true; + } + + this.index = start; + + return false; + } + + private boolean ipvFuture() { + int start = this.index; + + if (this.take('v') && this.hexDig()) { + while (this.hexDig()) { + // continue; + } + + if (this.take('.')) { + int j = 0; + + while (this.unreserved() || this.subDelims() || this.take(':')) { + j++; + } + + if (j >= 1) { + return true; + } + } + } + + this.index = start; + + return false; + } + + private boolean regName() { + int start = this.index; + + while (true) { + if (this.unreserved() || this.pctEncoded() || this.subDelims()) { + continue; + } + + if (this.isAuthorityEnd()) { + // End of authority + return true; + } + + if (this.str.charAt(this.index) == ':') { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean isPathEnd() { + if (this.index >= this.str.length()) { + return true; + } + + char c = this.str.charAt(this.index); + + System.err.println("path ned fail"); + return (c == '?' || c == '#'); + } + + private boolean pathAbempty() { + int start = this.index; + + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + + this.index = start; + + return false; + } + + private boolean pathAbsolute() { + int start = this.index; + + if (this.take('/')) { + if (this.segmentNz()) { + while (this.take('/') && this.segment()) { + // continue + } + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + System.err.println("path abs fail"); + return false; + } + + private boolean pathNoscheme() { + int start = this.index; + + if (this.segmentNzNc()) { + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + return false; + } + + private boolean pathRootless() { + int start = this.index; + + if (this.segmentNz()) { + while (this.take('/') && this.segment()) { + // continue + } + + if (this.isPathEnd()) { + return true; + } + } + + this.index = start; + + System.err.println("path root fail"); + return false; + } + + private boolean pathEmpty() { + return this.isPathEnd(); + } + + private boolean segment() { + while (this.pchar()) { + // continue + } + + return true; + } + + private boolean segmentNz() { + int start = this.index; + + if (this.pchar()) { + while (this.pchar()) { + // continue + } + return true; + } + + this.index = start; + + return false; + } + + private boolean segmentNzNc() { + int start = this.index; + + while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) { + // continue + } + + if (this.index - start > 0) { + return true; + } + + this.index = start; + + return false; + } + + private boolean pchar() { + return (this.unreserved() + || this.pctEncoded() + || this.subDelims() + || this.take(':') + || this.take('@')); + } + + private boolean query() { + int start = this.index; + + while (true) { + if (this.pchar() || this.take('/') || this.take('?')) { + continue; + } + + if (this.index == this.str.length() || this.str.charAt(this.index) == '#') { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean fragment() { + int start = this.index; + + while (true) { + if (this.pchar() || this.take('/') || this.take('?')) { + continue; + } + + if (this.index == this.str.length()) { + return true; + } + + this.index = start; + + return false; + } + } + + private boolean pctEncoded() { + int start = this.index; + + if (this.take('%') && this.hexDig() && this.hexDig()) { + this.pctEncodedFound = true; + + return true; + } + + this.index = start; + + return false; + } + + private boolean unreserved() { + return (this.alpha() + || this.digit() + || this.take('-') + || this.take('_') + || this.take('.') + || this.take('~')); + } + + private boolean subDelims() { + return (this.take('!') + || this.take('$') + || this.take('&') + || this.take('\'') + || this.take('(') + || this.take(')') + || this.take('*') + || this.take('+') + || this.take(',') + || this.take(';') + || this.take('=')); + } + + private boolean alpha() { + if (this.index >= this.str.length()) { + return false; + } + + char c = this.str.charAt(this.index); + + if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { + this.index++; + return true; + } + + return false; + } + + /** + * Reports whether the current position is a hex digit. + * + *

Method parses the rule: + * + *

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

Method parses the rule: + * + *

DIGIT = %x30-39 ; 0-9 + */ + private boolean digit() { + if (this.index >= this.str.length()) { + return false; + } + + char c = this.str.charAt(this.index); + if ('0' <= c && c <= '9') { + this.index++; + return true; + } + return false; + } + + /** + * Take the given char at the current 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 b864462c6bd2730f1ed35ee57bf094cb4405b3b2 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 3 Apr 2025 13:52:45 -0400 Subject: [PATCH 20/26] Decoding --- conformance/expected-failures.yaml | 86 +------------ .../java/build/buf/protovalidate/Uri.java | 115 +++++++++++------- 2 files changed, 70 insertions(+), 131 deletions(-) diff --git a/conformance/expected-failures.yaml b/conformance/expected-failures.yaml index 5d92bdca..d9abc65a 100644 --- a/conformance/expected-failures.yaml +++ b/conformance/expected-failures.yaml @@ -108,95 +108,13 @@ custom_constraints: # | this.all(e, e == 1) # | ^ library/is_uri: - - invalid/host/c - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo@你好.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://2001:0db8:85a3:0000:0000:8a2e:0370:7334"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6_zone-id_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipv6_zone-id_unquoted - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%eth0]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_reg-name_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo%c3x%96"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/port/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:8a"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/port/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/userinfo_reserved_at - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://@@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - valid/host_ipfuture_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[vF.-!$&'()*+,;=._~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipfuture_long - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1234AF.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/host_ipfuture_short - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" + # - valid/host_reg-name_pct-encoded_ascii - valid/host_ipv6_zone-id_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%61%20%23]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" + - valid/host_reg-name_pct-encoded_utf8 - valid/host_ipv6_zone-id_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%c3%96]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - - valid/path-empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" library/is_uri_ref: - valid/empty_string - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - valid/path-empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" standard_constraints/ignore: - proto/2023/map/ignore_always/invalid/populated # input: [type.googleapis.com/buf.validate.conformance.cases.EditionsMapIgnoreAlways]:{val:{key:1 value:1}} diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index bc997df0..4970d4a0 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -14,6 +14,20 @@ package build.buf.protovalidate; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CoderResult; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + final class Uri { private String str; private int index; @@ -35,22 +49,18 @@ boolean uri() { if (!(this.scheme() && this.take(':') && this.hierPart())) { this.index = start; - System.err.println("failed hier"); return false; } if (this.take('?') && !this.query()) { - System.err.println("failed cuir"); return false; } if (this.take('#') && !this.fragment()) { - System.err.println("failed fragomen"); return false; } if (this.index != this.str.length()) { - System.err.println("failed index"); this.index = start; return false; } @@ -71,7 +81,6 @@ private boolean hierPart() { int start = this.index; if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { - System.err.println("trooby"); return true; } @@ -154,28 +163,24 @@ private boolean authority() { if (this.userinfo()) { if (!this.take('@')) { this.index = start; - System.err.println("auth1 fail"); return false; } } if (!this.host()) { this.index = start; - System.err.println("auth2 fail"); return false; } if (this.take(':')) { if (!this.port()) { this.index = start; - System.err.println("auth3 fail"); return false; } } if (!this.isAuthorityEnd()) { this.index = start; - System.err.println("auth4 fail"); return false; } @@ -215,49 +220,69 @@ interface UnhexOperation { int unhex(char c); } - // private boolean checkHostPctEncoded(String str) { + private boolean checkHostPctEncoded(String str) { + + UnhexOperation fn = + c -> { + if ('0' <= c && c <= '9') { + return c - '0'; + } else if ('a' <= c && c <= 'f') { + return c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } + + return 0; + }; + + try { + ByteBuffer buffer = ByteBuffer.allocate(str.getBytes(StandardCharsets.UTF_8.toString()).length * Integer.BYTES); + + for (int i = 0; i < str.length(); ) { + if (str.charAt(i) == '%') { + buffer.putInt(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2))); + i += 3; + } else { + buffer.putInt((int) str.charAt(i)); + i++; + } + } - // UnhexOperation fn = - // c -> { - // if ('0' <= c && c <= '9') { - // return c - '0'; - // } else if ('a' <= c && c <= 'f') { - // return c - 'a' + 10; - // } else if ('A' <= c && c <= 'F') { - // return c - 'A' + 10; - // } + System.err.println(buffer); + System.err.println(buffer); + CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); + // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input + // decoder.onUnmappableCharacter(CodingErrorAction.REPORT); - // return 0; - // }; + CharBuffer out = CharBuffer.allocate(1024); - // List escaped = new ArrayList(); + CoderResult f = decoder.decode(buffer, out, true); + System.err.println(f.isMalformed()); + CoderResult fl = decoder.flush(out); - // for (int i = 0; i < str.length(); ) { - // if (str.charAt(i) == '%') { - // escaped.add(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2))); - // i += 3; - // } else { - // escaped.add((int) str.charAt(i)); - // i++; - // } - // } + System.err.println(fl.isMalformed()); + } catch (UnsupportedEncodingException uee) { + System.err.println("excepsh uee"); + } - // CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); + return true; - // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input - // decoder.onUnmappableCharacter(CodingErrorAction.REPORT); - // try { - // decoder.decode(java.nio.ByteBuffer.wrap(escaped)); // Attempt to decode - // return true; // No errors means valid UTF-8 - // } catch (Exception e) { - // return false; // Exception means invalid UTF-8 - // } - // System.err.println(escaped); + // System.err.println("dag af"); + // System.err.println(buffer); - // return true; - // } + // try { + // System.err.println("bout to get it"); + // CharBuffer buf = decoder.decode(buffer); // Attempt to decode + // System.err.println("done got it"); + // System.err.println(buf); + // return true; // No errors means valid UTF-8 + // } catch (Exception e) { + // System.err.println("no fail"); + // return false; // Exception means invalid UTF-8 + // } + } private boolean host() { if (this.index >= this.str.length()) { @@ -337,7 +362,6 @@ private boolean ipLiteral() { this.index = start; - System.err.println("litch fail"); return false; } @@ -441,7 +465,6 @@ private boolean isPathEnd() { char c = this.str.charAt(this.index); - System.err.println("path ned fail"); return (c == '?' || c == '#'); } @@ -478,7 +501,6 @@ private boolean pathAbsolute() { this.index = start; - System.err.println("path abs fail"); return false; } @@ -515,7 +537,6 @@ private boolean pathRootless() { this.index = start; - System.err.println("path root fail"); return false; } From 8d45700a438e086ca99ff2f3a9cd761af35e8db6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 3 Apr 2025 15:31:43 -0400 Subject: [PATCH 21/26] Cleanup --- conformance/expected-failures.yaml | 8 - .../buf/protovalidate/CustomOverload.java | 40 +- .../java/build/buf/protovalidate/Uri.java | 367 ++++++++++++++---- .../buf/protovalidate/CustomOverloadTest.java | 339 ++++++++-------- 4 files changed, 480 insertions(+), 274 deletions(-) diff --git a/conformance/expected-failures.yaml b/conformance/expected-failures.yaml index d9abc65a..0db100ef 100644 --- a/conformance/expected-failures.yaml +++ b/conformance/expected-failures.yaml @@ -107,14 +107,6 @@ custom_constraints: #ERROR: :1:1: expression of type 'int' cannot be range of a comprehension (must be list, map, or dynamic) # | this.all(e, e == 1) # | ^ -library/is_uri: - # - valid/host_reg-name_pct-encoded_ascii - - valid/host_ipv6_zone-id_pct-encoded_ascii - - valid/host_reg-name_pct-encoded_utf8 - - valid/host_ipv6_zone-id_pct-encoded_utf8 -library/is_uri_ref: - - valid/empty_string - - valid/path-empty standard_constraints/ignore: - proto/2023/map/ignore_always/invalid/populated # input: [type.googleapis.com/buf.validate.conformance.cases.EditionsMapIgnoreAlways]:{val:{key:1 value:1}} diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java index f23f6f5e..c2f18b5e 100644 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ b/src/main/java/build/buf/protovalidate/CustomOverload.java @@ -260,14 +260,14 @@ private static Overload celIsIp() { return Err.noSuchOverload(value, OVERLOAD_IS_IP, null); } String addr = (String) value.value(); - return Types.boolOf(isIP(addr, 0L)); + return Types.boolOf(isIp(addr, 0L)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String || rhs.type().typeEnum() != TypeEnum.Int) { return Err.noSuchOverload(lhs, OVERLOAD_IS_IP, rhs); } String address = (String) lhs.value(); - return Types.boolOf(isIP(address, rhs.intValue())); + return Types.boolOf(isIp(address, rhs.intValue())); }, null); } @@ -287,7 +287,7 @@ private static Overload celIsIpPrefix() { return Err.noSuchOverload(value, OVERLOAD_IS_IP_PREFIX, null); } String prefix = (String) value.value(); - return Types.boolOf(isIPPrefix(prefix, 0L, false)); + return Types.boolOf(isIpPrefix(prefix, 0L, false)); }, (lhs, rhs) -> { if (lhs.type().typeEnum() != TypeEnum.String @@ -297,9 +297,9 @@ private static Overload celIsIpPrefix() { } String prefix = (String) lhs.value(); if (rhs.type().typeEnum() == TypeEnum.Int) { - return Types.boolOf(isIPPrefix(prefix, rhs.intValue(), false)); + return Types.boolOf(isIpPrefix(prefix, rhs.intValue(), false)); } - return Types.boolOf(isIPPrefix(prefix, 0L, rhs.booleanValue())); + return Types.boolOf(isIpPrefix(prefix, 0L, rhs.booleanValue())); }, (values) -> { if (values.length != 3 @@ -309,7 +309,7 @@ private static Overload celIsIpPrefix() { return Err.noSuchOverload(values[0], OVERLOAD_IS_IP_PREFIX, "", values); } String prefix = (String) values[0].value(); - return Types.boolOf(isIPPrefix(prefix, values[1].intValue(), values[2].booleanValue())); + return Types.boolOf(isIpPrefix(prefix, values[1].intValue(), values[2].booleanValue())); }); } @@ -326,10 +326,7 @@ private static Overload celIsUri() { return Err.noSuchOverload(value, OVERLOAD_IS_URI, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(isURI(addr)); + return Types.boolOf(isUri(addr)); }); } @@ -346,10 +343,7 @@ private static Overload celIsUriRef() { return Err.noSuchOverload(value, OVERLOAD_IS_URI_REF, null); } String addr = (String) value.value(); - if (addr.isEmpty()) { - return BoolT.False; - } - return Types.boolOf(isURIRef(addr)); + return Types.boolOf(isUriRef(addr)); }); } @@ -446,21 +440,21 @@ private static boolean isHostAndPort(String str, boolean portRequired) { int endPlus = end + 1; if (endPlus == str.length()) { // no port - return !portRequired && isIP(str.substring(1, end), 6); + return !portRequired && isIp(str.substring(1, end), 6); } else if (endPlus == splitIdx) { // port - return isIP(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); + return isIp(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); } return false; // malformed } if (splitIdx < 0) { - return !portRequired && (isHostname(str) || isIP(str, 4)); + return !portRequired && (isHostname(str) || isIp(str, 4)); } String host = str.substring(0, splitIdx); String port = str.substring(splitIdx + 1); - return ((isHostname(host) || isIP(host, 4)) && isPort(port)); + return ((isHostname(host) || isIp(host, 4)) && isPort(port)); } // Returns true if the string is a valid port for isHostAndPort. @@ -604,7 +598,7 @@ private static boolean isHostname(String val) { *

Both formats are well-defined in the internet standard RFC 3986. Zone identifiers for IPv6 * addresses (for example "fe80::a%en1") are supported. */ - 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) { @@ -621,7 +615,7 @@ static boolean isIP(String addr, long ver) { *

URI is defined in the internet standard RFC 3986. Zone Identifiers in IPv6 address literals * are supported (RFC 6874). */ - public static boolean isURI(String str) { + private static boolean isUri(String str) { return new Uri(str).uri(); } @@ -632,7 +626,7 @@ public static boolean isURI(String str) { *

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) { + private static boolean isUriRef(String str) { return new Uri(str).uriReference(); } @@ -653,7 +647,7 @@ private static boolean isURIRef(String str) { *

The same principle applies to IPv4 addresses. "192.168.1.0/24" designates the first 24 bits * of the 32-bit IPv4 as the network prefix. */ - private static boolean isIPPrefix(String str, long version, boolean strict) { + private static boolean isIpPrefix(String str, long version, boolean strict) { if (version == 6L) { Ipv6 ip = new Ipv6(str); return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); @@ -661,7 +655,7 @@ private static boolean isIPPrefix(String str, long version, boolean strict) { Ipv4 ip = new Ipv4(str); return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); } else if (version == 0L) { - return isIPPrefix(str, 6, strict) || isIPPrefix(str, 4, strict); + return isIpPrefix(str, 6, strict) || isIpPrefix(str, 4, strict); } return false; } diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index 4970d4a0..6cf68a53 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -14,27 +14,24 @@ package build.buf.protovalidate; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.CoderResult; -import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; +import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; final class Uri { private String str; private int index; private boolean pctEncodedFound; + private CharsetDecoder utf8Decoder; + Uri(String str) { this.str = str; + // By default, CharsetDecoders will report on malformed input and unmappable characters. + this.utf8Decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); } /** @@ -42,7 +39,7 @@ final class Uri { * *

Method parses the rule: * - *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
    */
   boolean uri() {
     int start = this.index;
@@ -73,7 +70,10 @@ boolean uri() {
    *
    * 

Method parses the rule: * - *

hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty + *

hier-part = "//" authority path-abempty
+   *                / path-absolute
+   *                / path-rootless
+   *                / path-empty
    */
   // The multiple take('/') invocations are intended.
   @SuppressWarnings("IdentityBinaryExpression")
@@ -94,12 +94,19 @@ private boolean hierPart() {
    *
    * 

Method parses the rule: * - *

URI-reference = URI / relative-ref + *

URI-reference = URI / relative-ref
    */
   boolean uriReference() {
     return this.uri() || this.relativeRef();
   }
 
+  /**
+   * Reports whether string contains a valid relative reference.
+   *
+   * 

Method parses the rule: + * + *

relative-ref = relative-part [ "?" query ] [ "#" fragment ].
+   */
   private boolean relativeRef() {
     int start = this.index;
 
@@ -125,6 +132,16 @@ private boolean relativeRef() {
     return true;
   }
 
+  /**
+   * Reports whether string contains a valid relative part.
+   *
+   * 

Method parses the rule: + * + *

relative-part = "//" authority path-abempty
+   *                    / path-absolute
+   *                    / path-noscheme
+   *                    / path-empty
+   */
   // The multiple take('/') invocations are intended.
   @SuppressWarnings("IdentityBinaryExpression")
   private boolean relativePart() {
@@ -139,6 +156,13 @@ private boolean relativePart() {
     return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty();
   }
 
+  /**
+   * Reports whether string contains a valid scheme.
+   *
+   * 

Method parses the rule: + * + *

scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+   */
   private boolean scheme() {
     int start = this.index;
 
@@ -157,6 +181,15 @@ private boolean scheme() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid authority.
+   *
+   * 

Method parses the rule: + * + *

authority = [ userinfo "@" ] host [ ":" port ]
+   *
+   * Lead by double slash ("") and terminated by "/", "?", "#", or end of URI.
+   */
   private boolean authority() {
     int start = this.index;
 
@@ -187,6 +220,18 @@ private boolean authority() {
     return true;
   }
 
+  /**
+   * Reports whether the current position is the end of the authority.
+   *
+   * 

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

    + *
  • the next slash ("/") + *
  • question mark ("?") + *
  • number sign ("#") character + *
  • the end of the URI. + *
+ */ private boolean isAuthorityEnd() { if (this.index >= this.str.length()) { return true; @@ -195,6 +240,15 @@ private boolean isAuthorityEnd() { return (c == '?' || c == '#' || c == '/'); } + /** + * Reports whether string contains a valid userinfo. + * + *

Method parses the rule: + * + *

userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+   *
+   * Terminated by "@" in authority.
+   */
   private boolean userinfo() {
     int start = this.index;
 
@@ -220,8 +274,8 @@ interface UnhexOperation {
     int unhex(char c);
   }
 
+  /** Verifies that str is correctly percent-encoded. */
   private boolean checkHostPctEncoded(String str) {
-
     UnhexOperation fn =
         c -> {
           if ('0' <= c && c <= '9') {
@@ -235,55 +289,54 @@ private boolean checkHostPctEncoded(String str) {
           return 0;
         };
 
-    try {
-    ByteBuffer buffer = ByteBuffer.allocate(str.getBytes(StandardCharsets.UTF_8.toString()).length * Integer.BYTES);
+    // Reset the decoder before use.
+    this.utf8Decoder.reset();
 
+    int strLen = str.length();
+    ByteBuffer buffer = ByteBuffer.allocate(strLen);
+    CharBuffer out = CharBuffer.allocate(strLen);
+
+    // Unhex str and convert to a ByteBuffer.
     for (int i = 0; i < str.length(); ) {
       if (str.charAt(i) == '%') {
-        buffer.putInt(fn.unhex(str.charAt(i + 1)) << 4 | fn.unhex(str.charAt(i + 2)));
+        // If we encounter a %, unhex the two following digits, extract their
+        // last 4 bits, cast to a byte.
+        byte b =
+            (byte)
+                (((fn.unhex(str.charAt(i + 1)) & 0xf) << 4)
+                    | ((fn.unhex(str.charAt(i + 2)) & 0xf) << 0));
+        buffer.put(b);
         i += 3;
       } else {
-        buffer.putInt((int) str.charAt(i));
+        // Not percent encoded, extract the last 4 bits, convert to a byte
+        // and add to the byte buffer.
+        buffer.put((byte) (str.charAt(i) & 0xf));
         i++;
       }
     }
 
-    System.err.println(buffer);
-    System.err.println(buffer);
-    CharsetDecoder decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder();
-    // decoder.onMalformedInput(CodingErrorAction.REPORT); // Reject invalid input
-    // decoder.onUnmappableCharacter(CodingErrorAction.REPORT);
-
-    CharBuffer out = CharBuffer.allocate(1024);
-
-    CoderResult f = decoder.decode(buffer, out, true);
-    System.err.println(f.isMalformed());
-    CoderResult fl = decoder.flush(out);
+    // Attempt to decode the byte buffer as UTF-8.
+    CoderResult f = this.utf8Decoder.decode(buffer.flip(), out, true);
 
-    System.err.println(fl.isMalformed());
-    } catch (UnsupportedEncodingException uee) {
-        System.err.println("excepsh uee");
+    // If an error occurred, return false as invalid.
+    if (f.isError()) {
+      return false;
     }
+    // Flush the buffer
+    f = this.utf8Decoder.flush(out);
 
-    return true;
-
-
-
-    // System.err.println("dag af");
-    // System.err.println(buffer);
-
-    // try {
-    // System.err.println("bout to get it");
-    //   CharBuffer buf = decoder.decode(buffer); // Attempt to decode
-    // System.err.println("done got it");
-    // System.err.println(buf);
-    //   return true; // No errors means valid UTF-8
-    // } catch (Exception e) {
-    // System.err.println("no fail");
-    //   return false; // Exception means invalid UTF-8
-    // }
+    // If an error occurred, return false as invalid.
+    // Otherwise return true.
+    return !f.isError();
   }
 
+  /**
+   * Reports whether string contains a valid host.
+   *
+   * 

Method parses the rule: + * + *

host = IP-literal / IPv4address / reg-name.
+   */
   private boolean host() {
     if (this.index >= this.str.length()) {
       return true;
@@ -296,21 +349,12 @@ private boolean host() {
     if ((this.str.charAt(this.index) == '[' && this.ipLiteral()) || this.regName()) {
       if (this.pctEncodedFound) {
         String rawHost = this.str.substring(start, this.index);
-
+        // RFC 3986:
+        // > URI producing applications must not use percent-encoding in host
+        // > unless it is used to represent a UTF-8 character sequence.
         if (!this.checkHostPctEncoded(rawHost)) {
           return false;
         }
-        // try {
-        //   String s = URLDecoder.decode(rawHost, StandardCharsets.UTF_8.toString());
-        //   System.err.println(s);
-        // } catch (IllegalArgumentException e) {
-        //     System.err.println("CRASH");
-        //     return false;
-
-        // } catch (UnsupportedEncodingException e) {
-        //     System.err.println("BOOM");
-        //   return false;
-        // }
       }
 
       return true;
@@ -319,6 +363,15 @@ private boolean host() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid port.
+   *
+   * 

Method parses the rule: + * + *

port = *DIGIT
+   *
+   * Terminated by end of authority.
+   */
   private boolean port() {
     int start = this.index;
 
@@ -337,6 +390,13 @@ private boolean port() {
     }
   }
 
+  /**
+   * Reports whether string contains a valid IP literal.
+   *
+   * 

Method parses the rule from RFC 6874: + * + *

IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"
+   */
   private boolean ipLiteral() {
     int start = this.index;
 
@@ -365,6 +425,13 @@ private boolean ipLiteral() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid ipv6 address.
+   *
+   * 

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

Relies on the implementation of isIp. + */ private boolean ipv6Address() { int start = this.index; @@ -372,7 +439,7 @@ private boolean ipv6Address() { // continue } - if (CustomOverload.isIP(this.str.substring(start, this.index), 6)) { + if (CustomOverload.isIp(this.str.substring(start, this.index), 6)) { return true; } @@ -381,6 +448,13 @@ private boolean ipv6Address() { return false; } + /** + * Reports whether string contains a valid IPv6addrz. + * + * Method parses the rule: + * + *

IPv6addrz = IPv6address "%25" ZoneID
+   */
   private boolean ipv6Addrz() {
     int start = this.index;
 
@@ -393,6 +467,13 @@ private boolean ipv6Addrz() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid zone ID.
+   *
+   * Method parses the rule:
+   *
+   * 
ZoneID = 1*( unreserved / pct-encoded )
+   */
   private boolean zoneID() {
     int start = this.index;
 
@@ -409,6 +490,13 @@ private boolean zoneID() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid IPvFuture.
+   *
+   * Method parses the rule:
+   *
+   * 
IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+   */
   private boolean ipvFuture() {
     int start = this.index;
 
@@ -435,6 +523,15 @@ private boolean ipvFuture() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid reg-name.
+   *
+   * Method parses the rule:
+   *
+   * 
reg-name = *( unreserved / pct-encoded / sub-delims )
+   *
+   * Terminates on start of port (":") or end of authority.
+   */
   private boolean regName() {
     int start = this.index;
 
@@ -458,6 +555,17 @@ private boolean regName() {
     }
   }
 
+  /**
+   * Reports whether the current position is the end of the path.
+   *
+   * 

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

    + *
  • the first question mark ("?") + *
  • number sign ("#") character + *
  • the end of the URI. + *
+ */ private boolean isPathEnd() { if (this.index >= this.str.length()) { return true; @@ -468,6 +576,15 @@ private boolean isPathEnd() { return (c == '?' || c == '#'); } + /** + * Reports whether string contains a valid path-abempty. + * + * Method parses the rule: + * + *
path-abempty = *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
   private boolean pathAbempty() {
     int start = this.index;
 
@@ -484,6 +601,15 @@ private boolean pathAbempty() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid path-absolute.
+   *
+   * Method parses the rule:
+   *
+   * 
path-absolute = "/" [ segment-nz *( "/" segment ) ]
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
   private boolean pathAbsolute() {
     int start = this.index;
 
@@ -504,6 +630,15 @@ private boolean pathAbsolute() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid path-noscheme.
+   *
+   * Method parses the rule:
+   *
+   * 
path-noscheme = segment-nz-nc *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
   private boolean pathNoscheme() {
     int start = this.index;
 
@@ -522,6 +657,15 @@ private boolean pathNoscheme() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid path-rootless.
+   *
+   * Method parses the rule:
+   *
+   * 
path-rootless = segment-nz *( "/" segment )
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
   private boolean pathRootless() {
     int start = this.index;
 
@@ -540,25 +684,44 @@ private boolean pathRootless() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid path-empty.
+   *
+   * Method parses the rule:
+   *
+   * 
path-empty = 0
+   *
+   * Terminated by end of path: "?", "#", or end of URI.
+   */
   private boolean pathEmpty() {
     return this.isPathEnd();
   }
 
+  /**
+   * Reports whether string contains a valid segment.
+   *
+   * Method parses the rule:
+   *
+   * 
segment = *pchar
+   */
   private boolean segment() {
-    while (this.pchar()) {
-      // continue
-    }
+    while (this.pchar()) {}
 
     return true;
   }
 
+  /**
+   * Reports whether string contains a valid segment-nz.
+   *
+   * Method parses the rule:
+   *
+   * 
segment-nz = 1*pchar
+   */
   private boolean segmentNz() {
     int start = this.index;
 
     if (this.pchar()) {
-      while (this.pchar()) {
-        // continue
-      }
+      while (this.pchar()) {}
       return true;
     }
 
@@ -567,12 +730,18 @@ private boolean segmentNz() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid segment-nz-nc.
+   *
+   * Method parses the rule:
+   *
+   * 
segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+   *                   ; non-zero-length segment without any colon ":"
+   */
   private boolean segmentNzNc() {
     int start = this.index;
 
-    while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) {
-      // continue
-    }
+    while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) {}
 
     if (this.index - start > 0) {
       return true;
@@ -583,6 +752,13 @@ private boolean segmentNzNc() {
     return false;
   }
 
+  /**
+   * Reports whether string contains a valid pchar.
+   *
+   * Method parses the rule:
+   *
+   * 
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+   */
   private boolean pchar() {
     return (this.unreserved()
         || this.pctEncoded()
@@ -591,6 +767,15 @@ private boolean pchar() {
         || this.take('@'));
   }
 
+  /**
+   * Reports whether string contains a valid query.
+   *
+   * Method parses the rule:
+   *
+   * 
query = *( pchar / "/" / "?" )
+   *
+   * Terminated by "#" or end of URI.
+   */
   private boolean query() {
     int start = this.index;
 
@@ -609,6 +794,15 @@ private boolean query() {
     }
   }
 
+  /**
+   * Reports whether string contains a valid fragment.
+   *
+   * Method parses the rule:
+   *
+   * 
fragment = *( pchar / "/" / "?" )
+   *
+   * Terminated by end of URI.
+   */
   private boolean fragment() {
     int start = this.index;
 
@@ -627,6 +821,15 @@ private boolean fragment() {
     }
   }
 
+  /**
+   * Reports whether string contains a valid pct-encoded.
+   *
+   * Method parses the rule:
+   *
+   * 
pct-encoded = "%"+HEXDIG+HEXDIG
+   *
+   * Sets `pctEncodedFound` to true if a valid triplet was found.
+   */
   private boolean pctEncoded() {
     int start = this.index;
 
@@ -641,6 +844,13 @@ private boolean pctEncoded() {
     return false;
   }
 
+  /**
+   * Reports whether current position is an unreserved character.
+   *
+   * Method parses the rule:
+   *
+   * 
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+   */
   private boolean unreserved() {
     return (this.alpha()
         || this.digit()
@@ -650,6 +860,14 @@ private boolean unreserved() {
         || this.take('~'));
   }
 
+  /**
+   * Reports whether current position is a sub-delim.
+   *
+   * Method parses the rule:
+   *
+   * 
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
+   *                  / "*" / "+" / "," / ";" / "="
+   */
   private boolean subDelims() {
     return (this.take('!')
         || this.take('$')
@@ -664,6 +882,13 @@ private boolean subDelims() {
         || this.take('='));
   }
 
+  /**
+   * Reports whether current position is an alpha character.
+   *
+   * Method parses the rule:
+   *
+   * 
ALPHA =  %x41-5A / %x61-7A ; A-Z / a-z
+   */
   private boolean alpha() {
     if (this.index >= this.str.length()) {
       return false;
@@ -684,7 +909,7 @@ private boolean alpha() {
    *
    * 

Method parses the rule: * - *

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

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
    */
   private boolean hexDig() {
     if (this.index >= this.str.length()) {
@@ -706,7 +931,7 @@ private boolean hexDig() {
    *
    * 

Method parses the rule: * - *

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

DIGIT = %x30-39 ; 0-9
    */
   private boolean digit() {
     if (this.index >= this.str.length()) {
diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java
index 715f759b..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.isURI("https://[::1%25foo%c3x%96]");
-    assertThat(b).isFalse();
-    // 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 03ad6f2c37165a1803d6b8a15e16571055a4180e Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Thu, 3 Apr 2025 15:44:29 -0400
Subject: [PATCH 22/26] Cleanup

---
 build.gradle.kts                              |  9 ++--
 .../java/build/buf/protovalidate/Uri.java     | 44 ++++++++-----------
 2 files changed, 22 insertions(+), 31 deletions(-)

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/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java
index 6cf68a53..d60370a4 100644
--- a/src/main/java/build/buf/protovalidate/Uri.java
+++ b/src/main/java/build/buf/protovalidate/Uri.java
@@ -167,9 +167,7 @@ private boolean scheme() {
     int start = this.index;
 
     if (this.alpha()) {
-      while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) {
-        // continue
-      }
+      while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) {}
 
       if (this.str.charAt(this.index) == ':') {
         return true;
@@ -274,7 +272,17 @@ interface UnhexOperation {
     int unhex(char c);
   }
 
-  /** Verifies that str is correctly percent-encoded. */
+  /** 
+   * Verifies that str is correctly percent-encoded. 
+   *
+   * Note that we essentially want to mimic the behavior of decodeURIComponent, 
+   * which would fail on malformed URLs. Java does have various methods for 
+   * decoding URLs, but none behave consistently with decodeURIComponent.
+   *
+   * The code below is a combination of `checkHostPctEncoded` from the
+   * protovalidate-go implementation and Java's java.net.URI#decode methods.
+   *
+   **/
   private boolean checkHostPctEncoded(String str) {
     UnhexOperation fn =
         c -> {
@@ -435,9 +443,7 @@ private boolean ipLiteral() {
   private boolean ipv6Address() {
     int start = this.index;
 
-    while (this.hexDig() || this.take(':')) {
-      // continue
-    }
+    while (this.hexDig() || this.take(':')) {}
 
     if (CustomOverload.isIp(this.str.substring(start, this.index), 6)) {
       return true;
@@ -477,9 +483,7 @@ private boolean ipv6Addrz() {
   private boolean zoneID() {
     int start = this.index;
 
-    while (this.unreserved() || this.pctEncoded()) {
-      // continue
-    }
+    while (this.unreserved() || this.pctEncoded()) {}
 
     if (this.index - start > 0) {
       return true;
@@ -501,9 +505,7 @@ private boolean ipvFuture() {
     int start = this.index;
 
     if (this.take('v') && this.hexDig()) {
-      while (this.hexDig()) {
-        // continue;
-      }
+      while (this.hexDig()) {}
 
       if (this.take('.')) {
         int j = 0;
@@ -588,9 +590,7 @@ private boolean isPathEnd() {
   private boolean pathAbempty() {
     int start = this.index;
 
-    while (this.take('/') && this.segment()) {
-      // continue
-    }
+    while (this.take('/') && this.segment()) {}
 
     if (this.isPathEnd()) {
       return true;
@@ -615,9 +615,7 @@ private boolean pathAbsolute() {
 
     if (this.take('/')) {
       if (this.segmentNz()) {
-        while (this.take('/') && this.segment()) {
-          // continue
-        }
+        while (this.take('/') && this.segment()) {}
       }
 
       if (this.isPathEnd()) {
@@ -643,9 +641,7 @@ private boolean pathNoscheme() {
     int start = this.index;
 
     if (this.segmentNzNc()) {
-      while (this.take('/') && this.segment()) {
-        // continue
-      }
+      while (this.take('/') && this.segment()) {}
 
       if (this.isPathEnd()) {
         return true;
@@ -670,9 +666,7 @@ private boolean pathRootless() {
     int start = this.index;
 
     if (this.segmentNz()) {
-      while (this.take('/') && this.segment()) {
-        // continue
-      }
+      while (this.take('/') && this.segment()) {}
 
       if (this.isPathEnd()) {
         return true;

From 0f7106f9da19abb83e939e246c8007bc0dd72df3 Mon Sep 17 00:00:00 2001
From: Steve Ayers 
Date: Thu, 3 Apr 2025 15:52:28 -0400
Subject: [PATCH 23/26] Fix

---
 .../buf/protovalidate/CustomOverload.java     | 28 +++++++++----------
 .../java/build/buf/protovalidate/Uri.java     | 19 ++++++-------
 2 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java
index c2f18b5e..94cc4819 100644
--- a/src/main/java/build/buf/protovalidate/CustomOverload.java
+++ b/src/main/java/build/buf/protovalidate/CustomOverload.java
@@ -60,19 +60,19 @@ final class CustomOverload {
    */
   static Overload[] create() {
     return new Overload[] {
-      format(),
-      unique(),
-      startsWith(),
-      endsWith(),
-      contains(),
+      celFormat(),
+      celUnique(),
+      celStartsWith(),
+      celEndsWith(),
+      celContains(),
       celIsHostname(),
       celIsEmail(),
       celIsIp(),
       celIsIpPrefix(),
       celIsUri(),
       celIsUriRef(),
-      isNan(),
-      isInf(),
+      celIsNan(),
+      celIsInf(),
       celIsHostAndPort(),
     };
   }
@@ -82,7 +82,7 @@ static Overload[] create() {
    *
    * @return The {@link Overload} instance for the "format" operation.
    */
-  private static Overload format() {
+  private static Overload celFormat() {
     return Overload.binary(
         OVERLOAD_FORMAT,
         (lhs, rhs) -> {
@@ -104,7 +104,7 @@ private static Overload format() {
    *
    * @return The {@link Overload} instance for the "unique" operation.
    */
-  private static Overload unique() {
+  private static Overload celUnique() {
     return Overload.unary(
         OVERLOAD_UNIQUE,
         (val) -> {
@@ -120,7 +120,7 @@ private static Overload unique() {
    *
    * @return The {@link Overload} instance for the "startsWith" operation.
    */
-  private static Overload startsWith() {
+  private static Overload celStartsWith() {
     return Overload.binary(
         OVERLOAD_STARTS_WITH,
         (lhs, rhs) -> {
@@ -155,7 +155,7 @@ private static Overload startsWith() {
    *
    * @return The {@link Overload} instance for the "endsWith" operation.
    */
-  private static Overload endsWith() {
+  private static Overload celEndsWith() {
     return Overload.binary(
         OVERLOAD_ENDS_WITH,
         (lhs, rhs) -> {
@@ -190,7 +190,7 @@ private static Overload endsWith() {
    *
    * @return The {@link Overload} instance for the "contains" operation.
    */
-  private static Overload contains() {
+  private static Overload celContains() {
     return Overload.binary(
         OVERLOAD_CONTAINS,
         (lhs, rhs) -> {
@@ -352,7 +352,7 @@ private static Overload celIsUriRef() {
    *
    * @return The {@link Overload} instance for the "isNan" operation.
    */
-  private static Overload isNan() {
+  private static Overload celIsNan() {
     return Overload.unary(
         OVERLOAD_IS_NAN,
         value -> {
@@ -369,7 +369,7 @@ private static Overload isNan() {
    *
    * @return The {@link Overload} instance for the "isInf" operation.
    */
-  private static Overload isInf() {
+  private static Overload celIsInf() {
     return Overload.overload(
         OVERLOAD_IS_INF,
         null,
diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java
index d60370a4..83fc672b 100644
--- a/src/main/java/build/buf/protovalidate/Uri.java
+++ b/src/main/java/build/buf/protovalidate/Uri.java
@@ -272,17 +272,16 @@ interface UnhexOperation {
     int unhex(char c);
   }
 
-  /** 
-   * Verifies that str is correctly percent-encoded. 
-   *
-   * Note that we essentially want to mimic the behavior of decodeURIComponent, 
-   * which would fail on malformed URLs. Java does have various methods for 
-   * decoding URLs, but none behave consistently with decodeURIComponent.
+  /**
+   * Verifies that str is correctly percent-encoded.
    *
-   * The code below is a combination of `checkHostPctEncoded` from the
-   * protovalidate-go implementation and Java's java.net.URI#decode methods.
+   * 

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

The code below is a combination of `checkHostPctEncoded` from the protovalidate-go + * implementation and Java's java.net.URI#decode methods. + */ private boolean checkHostPctEncoded(String str) { UnhexOperation fn = c -> { @@ -324,7 +323,7 @@ private boolean checkHostPctEncoded(String str) { } // Attempt to decode the byte buffer as UTF-8. - CoderResult f = this.utf8Decoder.decode(buffer.flip(), out, true); + CoderResult f = this.utf8Decoder.decode((ByteBuffer)buffer.flip(), out, true); // If an error occurred, return false as invalid. if (f.isError()) { From 8017ce20b44af4fa3379a67322c5dc574af99285 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 4 Apr 2025 10:38:33 -0400 Subject: [PATCH 24/26] Fix --- conformance/src/main/java/build/.DS_Store | Bin 6148 -> 0 bytes .../java/build/buf/protovalidate/Uri.java | 55 +++++++----------- 2 files changed, 22 insertions(+), 33 deletions(-) delete mode 100644 conformance/src/main/java/build/.DS_Store diff --git a/conformance/src/main/java/build/.DS_Store b/conformance/src/main/java/build/.DS_Store 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/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index 83fc672b..a8f71295 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -16,7 +16,6 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; -import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; @@ -26,12 +25,8 @@ final class Uri { private int index; private boolean pctEncodedFound; - private CharsetDecoder utf8Decoder; - Uri(String str) { this.str = str; - // By default, CharsetDecoders will report on malformed input and unmappable characters. - this.utf8Decoder = Charset.forName(StandardCharsets.UTF_8.toString()).newDecoder(); } /** @@ -75,12 +70,10 @@ boolean uri() { * / path-rootless * / path-empty */ - // The multiple take('/') invocations are intended. - @SuppressWarnings("IdentityBinaryExpression") private boolean hierPart() { int start = this.index; - if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) { return true; } @@ -142,12 +135,10 @@ private boolean relativeRef() { * / path-noscheme * / path-empty */ - // The multiple take('/') invocations are intended. - @SuppressWarnings("IdentityBinaryExpression") private boolean relativePart() { int start = this.index; - if (this.take('/') && this.take('/') && this.authority() && this.pathAbempty()) { + if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) { return true; } @@ -156,6 +147,12 @@ private boolean relativePart() { return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty(); } + private boolean takeDoubleSlash() { + boolean isSlash = take('/'); + + return isSlash && take('/'); + } + /** * Reports whether string contains a valid scheme. * @@ -267,9 +264,16 @@ private boolean userinfo() { } } - @FunctionalInterface - interface UnhexOperation { - int unhex(char c); + private static int unhex(char c) { + if ('0' <= c && c <= '9') { + return c - '0'; + } else if ('a' <= c && c <= 'f') { + return c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } + + return 0; } /** @@ -283,21 +287,7 @@ interface UnhexOperation { * implementation and Java's java.net.URI#decode methods. */ private boolean checkHostPctEncoded(String str) { - UnhexOperation fn = - c -> { - if ('0' <= c && c <= '9') { - return c - '0'; - } else if ('a' <= c && c <= 'f') { - return c - 'a' + 10; - } else if ('A' <= c && c <= 'F') { - return c - 'A' + 10; - } - - return 0; - }; - - // Reset the decoder before use. - this.utf8Decoder.reset(); + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); int strLen = str.length(); ByteBuffer buffer = ByteBuffer.allocate(strLen); @@ -310,8 +300,7 @@ private boolean checkHostPctEncoded(String str) { // last 4 bits, cast to a byte. byte b = (byte) - (((fn.unhex(str.charAt(i + 1)) & 0xf) << 4) - | ((fn.unhex(str.charAt(i + 2)) & 0xf) << 0)); + (((unhex(str.charAt(i + 1)) & 0xf) << 4) | ((unhex(str.charAt(i + 2)) & 0xf) << 0)); buffer.put(b); i += 3; } else { @@ -323,14 +312,14 @@ private boolean checkHostPctEncoded(String str) { } // Attempt to decode the byte buffer as UTF-8. - CoderResult f = this.utf8Decoder.decode((ByteBuffer)buffer.flip(), out, true); + CoderResult f = decoder.decode((ByteBuffer) buffer.flip(), out, true); // If an error occurred, return false as invalid. if (f.isError()) { return false; } // Flush the buffer - f = this.utf8Decoder.flush(out); + f = decoder.flush(out); // If an error occurred, return false as invalid. // Otherwise return true. From 3d18da6a1005c2a2a55216d6e5266a2565448cae Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 4 Apr 2025 10:46:20 -0400 Subject: [PATCH 25/26] Lint --- src/main/java/build/buf/protovalidate/Uri.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index a8f71295..bbc1ea9a 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -148,9 +148,9 @@ private boolean relativePart() { } private boolean takeDoubleSlash() { - boolean isSlash = take('/'); + boolean isSlash = take('/'); - return isSlash && take('/'); + return isSlash && take('/'); } /** @@ -287,7 +287,7 @@ private static int unhex(char c) { * implementation and Java's java.net.URI#decode methods. */ private boolean checkHostPctEncoded(String str) { - CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); int strLen = str.length(); ByteBuffer buffer = ByteBuffer.allocate(strLen); From dcccf538876076184dc25868723cc39da17e1ede Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Fri, 4 Apr 2025 14:47:12 -0400 Subject: [PATCH 26/26] ... --- .../java/build/buf/protovalidate/Ipv4.java | 13 +- .../java/build/buf/protovalidate/Ipv6.java | 27 ++-- .../java/build/buf/protovalidate/Uri.java | 141 +++++++++--------- 3 files changed, 92 insertions(+), 89 deletions(-) diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java index 52f249a3..c3726b64 100644 --- a/src/main/java/build/buf/protovalidate/Ipv4.java +++ b/src/main/java/build/buf/protovalidate/Ipv4.java @@ -17,6 +17,9 @@ import java.util.ArrayList; import java.util.List; +/** + * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address prefix. + */ final class Ipv4 { private String str; private int index; @@ -76,7 +79,7 @@ boolean addressPrefix() { && this.index == this.str.length(); } - // Stores value in `prefixLen` + // Store value in prefixLen private boolean prefixLength() { int start = this.index; @@ -169,9 +172,9 @@ private boolean decOctet() { } /** - * Reports whether the current position is a digit. + * Determines whether the current position is a digit. * - *

Method parses the rule: + *

Parses the rule: * *

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

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

Method parses the rule: + *

Parses the rule: * *

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

Method parses the rule: + *

Parses the rule: * *

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

Method parses the rule: + *

Parses the rule: * *

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

Method parses the rule: + *

Parses the rule: * *

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

If char is at the current index, increment the index. + * Take the given char at the current position, incrementing the index if necessary. */ private boolean take(char c) { if (this.index >= this.str.length()) { diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java index bbc1ea9a..63287135 100644 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ b/src/main/java/build/buf/protovalidate/Uri.java @@ -20,6 +20,9 @@ import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; +/** + * Ipv6 is a class used to parse a given string to determine if it is a URI or URI reference. + */ final class Uri { private String str; private int index; @@ -30,9 +33,9 @@ final class Uri { } /** - * Reports whether string is a valid URI. + * Determines whether string is a valid URI. * - *

Method parses the rule: + *

Parses the rule: * *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
    */
@@ -61,9 +64,9 @@ boolean uri() {
   }
 
   /**
-   * Reports whether string contains a valid hier-part.
+   * Determines whether the current position is a valid hier-part.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

hier-part = "//" authority path-abempty
    *                / path-absolute
@@ -83,9 +86,9 @@ private boolean hierPart() {
   }
 
   /**
-   * Reports whether string is a valid URI reference.
+   * Determines whether string is a valid URI reference.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

URI-reference = URI / relative-ref
    */
@@ -94,9 +97,9 @@ boolean uriReference() {
   }
 
   /**
-   * Reports whether string contains a valid relative reference.
+   * Determines whether the current position is a valid relative reference.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

relative-ref = relative-part [ "?" query ] [ "#" fragment ].
    */
@@ -126,9 +129,9 @@ private boolean relativeRef() {
   }
 
   /**
-   * Reports whether string contains a valid relative part.
+   * Determines whether the current position is a valid relative part.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

relative-part = "//" authority path-abempty
    *                    / path-absolute
@@ -154,9 +157,9 @@ private boolean takeDoubleSlash() {
   }
 
   /**
-   * Reports whether string contains a valid scheme.
+   * Determines whether the current position is a valid scheme.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
    */
@@ -177,9 +180,9 @@ private boolean scheme() {
   }
 
   /**
-   * Reports whether string contains a valid authority.
+   * Determines whether the current position is a valid authority.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

authority = [ userinfo "@" ] host [ ":" port ]
    *
@@ -216,7 +219,7 @@ private boolean authority() {
   }
 
   /**
-   * Reports whether the current position is the end of the authority.
+   * Determines whether the current position is the end of the authority.
    *
    * 

The authority component [...] is terminated by one of the following: * @@ -236,9 +239,9 @@ private boolean isAuthorityEnd() { } /** - * Reports whether string contains a valid userinfo. + * Determines whether the current position is a valid userinfo. * - *

Method parses the rule: + *

Parses the rule: * *

userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
    *
@@ -327,9 +330,9 @@ private boolean checkHostPctEncoded(String str) {
   }
 
   /**
-   * Reports whether string contains a valid host.
+   * Determines whether the current position is a valid host.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

host = IP-literal / IPv4address / reg-name.
    */
@@ -360,9 +363,9 @@ private boolean host() {
   }
 
   /**
-   * Reports whether string contains a valid port.
+   * Determines whether the current position is a valid port.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

port = *DIGIT
    *
@@ -387,9 +390,9 @@ private boolean port() {
   }
 
   /**
-   * Reports whether string contains a valid IP literal.
+   * Determines whether the current position is a valid IP literal.
    *
-   * 

Method parses the rule from RFC 6874: + *

Parses the rule from RFC 6874: * *

IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"
    */
@@ -422,7 +425,7 @@ private boolean ipLiteral() {
   }
 
   /**
-   * Reports whether string contains a valid ipv6 address.
+   * Determines whether the current position is a valid ipv6 address.
    *
    * 

ipv6Address parses the rule "IPv6address". * @@ -443,9 +446,9 @@ private boolean ipv6Address() { } /** - * Reports whether string contains a valid IPv6addrz. + * Determines whether the current position is a valid IPv6addrz. * - * Method parses the rule: + * Parses the rule: * *

IPv6addrz = IPv6address "%25" ZoneID
    */
@@ -462,9 +465,9 @@ private boolean ipv6Addrz() {
   }
 
   /**
-   * Reports whether string contains a valid zone ID.
+   * Determines whether the current position is a valid zone ID.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
ZoneID = 1*( unreserved / pct-encoded )
    */
@@ -483,9 +486,9 @@ private boolean zoneID() {
   }
 
   /**
-   * Reports whether string contains a valid IPvFuture.
+   * Determines whether the current position is a valid IPvFuture.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
    */
@@ -514,9 +517,9 @@ private boolean ipvFuture() {
   }
 
   /**
-   * Reports whether string contains a valid reg-name.
+   * Determines whether the current position is a valid reg-name.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
reg-name = *( unreserved / pct-encoded / sub-delims )
    *
@@ -546,7 +549,7 @@ private boolean regName() {
   }
 
   /**
-   * Reports whether the current position is the end of the path.
+   * Determines whether the current position is the end of the path.
    *
    * 

The path is terminated by one of the following: * @@ -567,9 +570,9 @@ private boolean isPathEnd() { } /** - * Reports whether string contains a valid path-abempty. + * Determines whether the current position is a valid path-abempty. * - * Method parses the rule: + * Parses the rule: * *

path-abempty = *( "/" segment )
    *
@@ -590,9 +593,9 @@ private boolean pathAbempty() {
   }
 
   /**
-   * Reports whether string contains a valid path-absolute.
+   * Determines whether the current position is a valid path-absolute.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
path-absolute = "/" [ segment-nz *( "/" segment ) ]
    *
@@ -617,9 +620,9 @@ private boolean pathAbsolute() {
   }
 
   /**
-   * Reports whether string contains a valid path-noscheme.
+   * Determines whether the current position is a valid path-noscheme.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
path-noscheme = segment-nz-nc *( "/" segment )
    *
@@ -642,9 +645,9 @@ private boolean pathNoscheme() {
   }
 
   /**
-   * Reports whether string contains a valid path-rootless.
+   * Determines whether the current position is a valid path-rootless.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
path-rootless = segment-nz *( "/" segment )
    *
@@ -667,9 +670,9 @@ private boolean pathRootless() {
   }
 
   /**
-   * Reports whether string contains a valid path-empty.
+   * Determines whether the current position is a valid path-empty.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
path-empty = 0
    *
@@ -680,9 +683,9 @@ private boolean pathEmpty() {
   }
 
   /**
-   * Reports whether string contains a valid segment.
+   * Determines whether the current position is a valid segment.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
segment = *pchar
    */
@@ -693,9 +696,9 @@ private boolean segment() {
   }
 
   /**
-   * Reports whether string contains a valid segment-nz.
+   * Determines whether the current position is a valid segment-nz.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
segment-nz = 1*pchar
    */
@@ -713,9 +716,9 @@ private boolean segmentNz() {
   }
 
   /**
-   * Reports whether string contains a valid segment-nz-nc.
+   * Determines whether the current position is a valid segment-nz-nc.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
    *                   ; non-zero-length segment without any colon ":"
@@ -735,9 +738,9 @@ private boolean segmentNzNc() {
   }
 
   /**
-   * Reports whether string contains a valid pchar.
+   * Determines whether the current position is a valid pchar.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
    */
@@ -750,9 +753,9 @@ private boolean pchar() {
   }
 
   /**
-   * Reports whether string contains a valid query.
+   * Determines whether the current position is a valid query.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
query = *( pchar / "/" / "?" )
    *
@@ -777,9 +780,9 @@ private boolean query() {
   }
 
   /**
-   * Reports whether string contains a valid fragment.
+   * Determines whether the current position is a valid fragment.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
fragment = *( pchar / "/" / "?" )
    *
@@ -804,9 +807,9 @@ private boolean fragment() {
   }
 
   /**
-   * Reports whether string contains a valid pct-encoded.
+   * Determines whether the current position is a valid pct-encoded.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
pct-encoded = "%"+HEXDIG+HEXDIG
    *
@@ -827,9 +830,9 @@ private boolean pctEncoded() {
   }
 
   /**
-   * Reports whether current position is an unreserved character.
+   * Determines whether the current position is an unreserved character.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
    */
@@ -843,9 +846,9 @@ private boolean unreserved() {
   }
 
   /**
-   * Reports whether current position is a sub-delim.
+   * Determines whether the current position is a sub-delim.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
    *                  / "*" / "+" / "," / ";" / "="
@@ -865,9 +868,9 @@ private boolean subDelims() {
   }
 
   /**
-   * Reports whether current position is an alpha character.
+   * Determines whether the current position is an alpha character.
    *
-   * Method parses the rule:
+   * Parses the rule:
    *
    * 
ALPHA =  %x41-5A / %x61-7A ; A-Z / a-z
    */
@@ -887,9 +890,9 @@ private boolean alpha() {
   }
 
   /**
-   * Reports whether the current position is a hex digit.
+   * Determines whether the current position is a hex digit.
    *
-   * 

Method parses the rule: + *

Parses the rule: * *

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

Method parses the rule: + *

Parses the rule: * *

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

If char is at the current index, increment the index. + * Take the given char at the current position, incrementing the index if necessary. */ private boolean take(char c) { if (this.index >= this.str.length()) {