diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RuleArn.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RuleArn.java.resource index c3fbbb9db19b..1a89aad3e789 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RuleArn.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RuleArn.java.resource @@ -1,3 +1,4 @@ +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -21,21 +22,58 @@ public final class RuleArn { } public static RuleArn parse(String arn) { - String[] base = arn.split(":", 6); - if (base.length != 6) { + if (arn == null || arn.length() < 8 || !arn.startsWith("arn:")) { return null; } - // service, resource and `arn` may not be null - if (!base[0].equals("arn")) { + + // find each of the first five ':' positions + int p0 = 3; // after "arn" + int p1 = arn.indexOf(':', p0 + 1); + if (p1 < 0) { return null; } - if (base[1].isEmpty() || base[2].isEmpty()) { + + int p2 = arn.indexOf(':', p1 + 1); + if (p2 < 0) { return null; } - if (base[5].isEmpty()) { + + int p3 = arn.indexOf(':', p2 + 1); + if (p3 < 0) { return null; } - return new RuleArn(base[1], base[2], base[3], base[4], Arrays.asList(base[5].split("[:/]", -1))); + + int p4 = arn.indexOf(':', p3 + 1); + if (p4 < 0) { + return null; + } + + // extract and validate mandatory parts + String partition = arn.substring(p0 + 1, p1); + String service = arn.substring(p1 + 1, p2); + String region = arn.substring(p2 + 1, p3); + String accountId = arn.substring(p3 + 1, p4); + String resource = arn.substring(p4 + 1); + + if (partition.isEmpty() || service.isEmpty() || resource.isEmpty()) { + return null; + } + return new RuleArn(partition, service, region, accountId, splitResource(resource)); + } + + private static List splitResource(String resource) { + List result = new ArrayList<>(); + int start = 0; + int length = resource.length(); + for (int i = 0; i < length; i++) { + char c = resource.charAt(i); + if (c == ':' || c == '/') { + result.add(resource.substring(start, i)); + start = i + 1; + } + } + result.add(resource.substring(start)); + return result; } public String partition() { diff --git a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource index 5cfb8b857a5a..63b4969a9ccf 100644 --- a/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource +++ b/codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource @@ -6,49 +6,43 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; -import java.util.regex.Pattern; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.exception.SdkClientException; @SdkInternalApi public class RulesFunctions { - private static final Pattern VALID_HOST_LABEL_SUBDOMAINS = Pattern.compile("[a-zA-Z\\d][a-zA-Z\\d\\-.]{0,62}"); - private static final Pattern VALID_HOST_LABEL = Pattern.compile("[a-zA-Z\\d][a-zA-Z\\d\\-]{0,62}"); + private static final String[] ENCODED_CHARACTERS = { "+", "*", "%7E" }; + private static final String[] ENCODED_CHARACTERS_REPLACEMENTS = { "%20", "%2A", "~" }; - private static final Pattern VIRTUAL_HOSTABLE_BUCKET = Pattern.compile("[a-z\\d][a-z\\d\\-.]{1,61}[a-z\\d]"); - private static final Pattern VIRTUAL_HOSTABLE_BUCKET_NO_SUBDOMAINS = Pattern.compile("[a-z\\d][a-z\\d\\-]{1,61}[a-z\\d]"); - private static final Pattern NO_IPS = Pattern.compile("(\\d+\\.){3}\\d+"); - private static final Pattern NO_CONSECUTIVE_DASH_OR_DOTS = Pattern.compile(".*[.-]{2}.*"); + private static final LazyValue PARTITION_DATA = LazyValue. builder() + .initializer(RulesFunctions::loadPartitionData).build(); - private static final String[] ENCODED_CHARACTERS = {"+", "*", "%7E"}; - private static final String[] ENCODED_CHARACTERS_REPLACEMENTS = {"%20", "%2A", "~"}; + private static final LazyValue AWS_PARTITION = LazyValue. builder() + .initializer(RulesFunctions::findAwsPartition).build(); - private static final LazyValue PARTITION_DATA = LazyValue.builder() - .initializer(RulesFunctions::loadPartitionData).build(); - - private static final LazyValue AWS_PARTITION = LazyValue.builder() - .initializer(RulesFunctions::findAwsPartition).build(); - - public static String substring(String input, int start, int stop, boolean reverse) { - int len = input.length(); - if (start >= stop || len < stop) { + public static String substring(String value, int startIndex, int stopIndex, boolean reverse) { + if (value == null) { return null; } - int realStart = start; - int realStop = stop; - if (reverse) { - realStart = len - stop; - realStop = len - start; + + int len = value.length(); + if (startIndex >= stopIndex || len < stopIndex) { + return null; } - StringBuilder result = new StringBuilder(realStop - realStart); - for (int idx = realStart; idx < realStop; idx++) { - char ch = input.charAt(idx); - if (ch > 0x7F) { + + for (int i = 0; i < len; i++) { + if (value.charAt(i) > 127) { return null; } - result.append(ch); } - return result.toString(); + + if (reverse) { + int revStart = len - stopIndex; + int revStop = len - startIndex; + return value.substring(revStart, revStop); + } else { + return value.substring(startIndex, stopIndex); + } } // URI related functions @@ -72,11 +66,63 @@ public class RulesFunctions { } } - public static boolean isValidHostLabel(String value, boolean allowSubDomains) { - if (allowSubDomains) { - return VALID_HOST_LABEL_SUBDOMAINS.matcher(value).matches(); + public static boolean isValidHostLabel(String hostLabel, boolean allowDots) { + int len = hostLabel == null ? 0 : hostLabel.length(); + if (len == 0) { + return false; + } + + // Single-label mode + if (!allowDots) { + return isValidSingleLabel(hostLabel, 0, len); + } + + // Multi-label mode + int start = 0; + for (int i = 0; i <= len; i++) { + if (i == len || hostLabel.charAt(i) == '.') { + // chunk is hostLabel[start..i) + int chunkLen = i - start; + if (chunkLen < 1 || chunkLen > 63) { + return false; + } else if (!isValidSingleLabel(hostLabel, start, i)) { + return false; + } + start = i + 1; + } } - return VALID_HOST_LABEL.matcher(value).matches(); + return true; + } + + // Validates a single label in s[start..end): ^[A-Za-z0-9][A-Za-z0-9\-]{0,62}$ + private static boolean isValidSingleLabel(String s, int start, int end) { + int length = end - start; + if (length < 1 || length > 63) { + return false; + } + + // first char must be [A-Za-z0-9] + if (!isAlphanumeric(s.charAt(start))) { + return false; + } + + // remaining chars must be [A-Za-z0-9-] + for (int i = start + 1; i < end; i++) { + char c = s.charAt(i); + if (!isAlphanumeric(c) && c != '-') { + return false; + } + } + + return true; + } + + private static boolean isAlphanumeric(char c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + private static boolean isLowerCaseAlphanumeric(char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); } // AWS related functions @@ -126,13 +172,108 @@ public class RulesFunctions { return values.get(index); } - public static boolean awsIsVirtualHostableS3Bucket(String hostLabel, boolean allowSubDomains) { - if (allowSubDomains) { - return VIRTUAL_HOSTABLE_BUCKET.matcher(hostLabel).matches() - // don't allow ip address - && !NO_IPS.matcher(hostLabel).matches() && !NO_CONSECUTIVE_DASH_OR_DOTS.matcher(hostLabel).matches(); + public static boolean awsIsVirtualHostableS3Bucket(String hostLabel, boolean allowDots) { + // Bucket names must be between 3 (min) and 63 (max) characters long. + int bucketLength = hostLabel == null ? 0 : hostLabel.length(); + if (bucketLength < 3 || bucketLength > 63) { + return false; + } + + // Bucket names must begin and end with a letter or number. + if (!isLowerCaseAlphanumeric(hostLabel.charAt(0)) || !isLowerCaseAlphanumeric(hostLabel.charAt(bucketLength - 1))) { + return false; + } + + // Bucket names can consist only of lowercase letters, numbers, periods (.), and hyphens (-). + if (!allowDots) { + for (int i = 1; i < bucketLength - 1; i++) { // already validated 0 and N - 1. + if (!isValidBucketSegmentChar(hostLabel.charAt(i))) { + return false; + } + } + return true; + } + + // Check for consecutive dots or hyphens + char last = hostLabel.charAt(0); + for (int i = 1; i < bucketLength; i++) { + char c = hostLabel.charAt(i); + // Don't allow "bucket-.foo" or "bucket.-foo" + if (c == '.') { + if (last == '.' || last == '-') { + return false; + } + } else if (c == '-') { + if (last == '.') { + return false; + } + } else if (!isLowerCaseAlphanumeric(c)) { + return false; + } + last = c; + } + + // Bucket names must not be formatted as an IP address (for example, 192.168.5.4). + return !isIpAddr(hostLabel); + } + + private static boolean isValidBucketSegmentChar(char c) { + return isLowerCaseAlphanumeric(c) || c == '-'; + } + + private static boolean isIpAddr(String host) { + if (host == null || host.length() < 2) { + return false; + } + + // Simple check for IPv6 (enclosed in square brackets) + if (host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') { + return true; + } + + int from = 0; + int segments = 0; + boolean done = false; + while (!done) { + int index = host.indexOf('.', from); + if (index == -1) { + if (segments != 3) { + // E.g., 123.com + return false; + } + index = host.length(); + done = true; + } else if (segments == 3) { + // E.g., 1.2.3.4.5 + return false; + } + int length = index - from; + if (length == 1) { + char ch0 = host.charAt(from); + if (ch0 < '0' || ch0 > '9') { + return false; + } + } else if (length == 2) { + char ch0 = host.charAt(from); + char ch1 = host.charAt(from + 1); + if ((ch0 <= '0' || ch0 > '9') || (ch1 < '0' || ch1 > '9')) { + return false; + } + } else if (length == 3) { + char ch0 = host.charAt(from); + char ch1 = host.charAt(from + 1); + char ch2 = host.charAt(from + 2); + if ((ch0 <= '0' || ch0 > '9') || (ch1 < '0' || ch1 > '9') || (ch2 < '0' || ch2 > '9')) { + return false; + } + // This is a heuristic; We are intentionally not checking for the range 000-255. + } else { + return false; + } + from = index + 1; + segments += 1; } - return VIRTUAL_HOSTABLE_BUCKET_NO_SUBDOMAINS.matcher(hostLabel).matches(); + return true; } private static PartitionData loadPartitionData() {