Skip to content

Commit 2a8f873

Browse files
authored
Optimize Endpoint Rule Standard library functions (#6622)
* Optimize Endpoint Rule Standard library functions * Add changelog * Add constants
1 parent cb842a2 commit 2a8f873

File tree

3 files changed

+237
-45
lines changed

3 files changed

+237
-45
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Optimize endpoint rule standard functions"
6+
}

codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RuleArn.java.resource

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import java.util.ArrayList;
12
import java.util.Arrays;
23
import java.util.Collections;
34
import java.util.List;
@@ -12,6 +13,9 @@ public final class RuleArn {
1213
private final String accountId;
1314
private final List<String> resourceId;
1415

16+
private static final int ARN_MIN_LENGTH = 8;
17+
private static final int ARN_PREFIX_LENGTH = 3;
18+
1519
RuleArn(String partition, String service, String region, String accountId, List<String> resourceId) {
1620
this.partition = partition;
1721
this.service = service;
@@ -21,21 +25,58 @@ public final class RuleArn {
2125
}
2226

2327
public static RuleArn parse(String arn) {
24-
String[] base = arn.split(":", 6);
25-
if (base.length != 6) {
28+
if (arn == null || arn.length() < ARN_MIN_LENGTH || !arn.startsWith("arn:")) {
29+
return null;
30+
}
31+
32+
// find each of the first five ':' positions
33+
int p0 = ARN_PREFIX_LENGTH; // after "arn"
34+
int p1 = arn.indexOf(':', p0 + 1);
35+
if (p1 < 0) {
36+
return null;
37+
}
38+
39+
int p2 = arn.indexOf(':', p1 + 1);
40+
if (p2 < 0) {
2641
return null;
2742
}
28-
// service, resource and `arn` may not be null
29-
if (!base[0].equals("arn")) {
43+
44+
int p3 = arn.indexOf(':', p2 + 1);
45+
if (p3 < 0) {
3046
return null;
3147
}
32-
if (base[1].isEmpty() || base[2].isEmpty()) {
48+
49+
int p4 = arn.indexOf(':', p3 + 1);
50+
if (p4 < 0) {
3351
return null;
3452
}
35-
if (base[5].isEmpty()) {
53+
54+
// extract and validate mandatory parts
55+
String partition = arn.substring(p0 + 1, p1);
56+
String service = arn.substring(p1 + 1, p2);
57+
String region = arn.substring(p2 + 1, p3);
58+
String accountId = arn.substring(p3 + 1, p4);
59+
String resource = arn.substring(p4 + 1);
60+
61+
if (partition.isEmpty() || service.isEmpty() || resource.isEmpty()) {
3662
return null;
3763
}
38-
return new RuleArn(base[1], base[2], base[3], base[4], Arrays.asList(base[5].split("[:/]", -1)));
64+
return new RuleArn(partition, service, region, accountId, splitResource(resource));
65+
}
66+
67+
private static List<String> splitResource(String resource) {
68+
List<String> result = new ArrayList<>();
69+
int start = 0;
70+
int length = resource.length();
71+
for (int i = 0; i < length; i++) {
72+
char c = resource.charAt(i);
73+
if (c == ':' || c == '/') {
74+
result.add(resource.substring(start, i));
75+
start = i + 1;
76+
}
77+
}
78+
result.add(resource.substring(start));
79+
return result;
3980
}
4081

4182
public String partition() {

codegen/src/main/resources/software/amazon/awssdk/codegen/rules2/RulesFunctions.java.resource

Lines changed: 183 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,47 @@ import java.util.HashMap;
66
import java.util.List;
77
import java.util.Map;
88
import java.util.function.Supplier;
9-
import java.util.regex.Pattern;
109
import software.amazon.awssdk.annotations.SdkInternalApi;
1110
import software.amazon.awssdk.core.exception.SdkClientException;
1211

1312
@SdkInternalApi
1413
public class RulesFunctions {
15-
private static final Pattern VALID_HOST_LABEL_SUBDOMAINS = Pattern.compile("[a-zA-Z\\d][a-zA-Z\\d\\-.]{0,62}");
16-
private static final Pattern VALID_HOST_LABEL = Pattern.compile("[a-zA-Z\\d][a-zA-Z\\d\\-]{0,62}");
14+
private static final String[] ENCODED_CHARACTERS = { "+", "*", "%7E" };
15+
private static final String[] ENCODED_CHARACTERS_REPLACEMENTS = { "%20", "%2A", "~" };
1716

18-
private static final Pattern VIRTUAL_HOSTABLE_BUCKET = Pattern.compile("[a-z\\d][a-z\\d\\-.]{1,61}[a-z\\d]");
19-
private static final Pattern VIRTUAL_HOSTABLE_BUCKET_NO_SUBDOMAINS = Pattern.compile("[a-z\\d][a-z\\d\\-]{1,61}[a-z\\d]");
20-
private static final Pattern NO_IPS = Pattern.compile("(\\d+\\.){3}\\d+");
21-
private static final Pattern NO_CONSECUTIVE_DASH_OR_DOTS = Pattern.compile(".*[.-]{2}.*");
17+
private static final LazyValue<PartitionData> PARTITION_DATA = LazyValue.<PartitionData> builder()
18+
.initializer(RulesFunctions::loadPartitionData).build();
2219

23-
private static final String[] ENCODED_CHARACTERS = {"+", "*", "%7E"};
24-
private static final String[] ENCODED_CHARACTERS_REPLACEMENTS = {"%20", "%2A", "~"};
20+
private static final LazyValue<Partition> AWS_PARTITION = LazyValue.<Partition> builder()
21+
.initializer(RulesFunctions::findAwsPartition).build();
2522

26-
private static final LazyValue<PartitionData> PARTITION_DATA = LazyValue.<PartitionData>builder()
27-
.initializer(RulesFunctions::loadPartitionData).build();
23+
private static final int MAX_HOST_LABEL_SIZE = 63;
24+
private static final int MIN_BUCKET_SIZE = 3;
2825

29-
private static final LazyValue<Partition> AWS_PARTITION = LazyValue.<Partition>builder()
30-
.initializer(RulesFunctions::findAwsPartition).build();
31-
32-
public static String substring(String input, int start, int stop, boolean reverse) {
33-
int len = input.length();
34-
if (start >= stop || len < stop) {
26+
public static String substring(String value, int startIndex, int stopIndex, boolean reverse) {
27+
if (value == null) {
3528
return null;
3629
}
37-
int realStart = start;
38-
int realStop = stop;
39-
if (reverse) {
40-
realStart = len - stop;
41-
realStop = len - start;
30+
31+
int len = value.length();
32+
if (startIndex >= stopIndex || len < stopIndex) {
33+
return null;
4234
}
43-
StringBuilder result = new StringBuilder(realStop - realStart);
44-
for (int idx = realStart; idx < realStop; idx++) {
45-
char ch = input.charAt(idx);
46-
if (ch > 0x7F) {
35+
36+
for (int i = 0; i < len; i++) {
37+
// non-ascii characters (values outside of the 7bit ASCII range)
38+
if (value.charAt(i) > 127) {
4739
return null;
4840
}
49-
result.append(ch);
5041
}
51-
return result.toString();
42+
43+
if (reverse) {
44+
int revStart = len - stopIndex;
45+
int revStop = len - startIndex;
46+
return value.substring(revStart, revStop);
47+
} else {
48+
return value.substring(startIndex, stopIndex);
49+
}
5250
}
5351

5452
// URI related functions
@@ -72,11 +70,63 @@ public class RulesFunctions {
7270
}
7371
}
7472

75-
public static boolean isValidHostLabel(String value, boolean allowSubDomains) {
76-
if (allowSubDomains) {
77-
return VALID_HOST_LABEL_SUBDOMAINS.matcher(value).matches();
73+
public static boolean isValidHostLabel(String hostLabel, boolean allowDots) {
74+
int len = hostLabel == null ? 0 : hostLabel.length();
75+
if (len == 0) {
76+
return false;
77+
}
78+
79+
// Single-label mode
80+
if (!allowDots) {
81+
return isValidSingleLabel(hostLabel, 0, len);
82+
}
83+
84+
// Multi-label mode
85+
int start = 0;
86+
for (int i = 0; i <= len; i++) {
87+
if (i == len || hostLabel.charAt(i) == '.') {
88+
// chunk is hostLabel[start..i)
89+
int chunkLen = i - start;
90+
if (chunkLen < 1 || chunkLen > MAX_HOST_LABEL_SIZE) {
91+
return false;
92+
} else if (!isValidSingleLabel(hostLabel, start, i)) {
93+
return false;
94+
}
95+
start = i + 1;
96+
}
97+
}
98+
return true;
99+
}
100+
101+
// Validates a single label in s[start..end): ^[A-Za-z0-9][A-Za-z0-9\-]{0,62}$
102+
private static boolean isValidSingleLabel(String s, int start, int end) {
103+
int length = end - start;
104+
if (length < 1 || length > MAX_HOST_LABEL_SIZE) {
105+
return false;
106+
}
107+
108+
// first char must be [A-Za-z0-9]
109+
if (!isAlphanumeric(s.charAt(start))) {
110+
return false;
78111
}
79-
return VALID_HOST_LABEL.matcher(value).matches();
112+
113+
// remaining chars must be [A-Za-z0-9-]
114+
for (int i = start + 1; i < end; i++) {
115+
char c = s.charAt(i);
116+
if (!isAlphanumeric(c) && c != '-') {
117+
return false;
118+
}
119+
}
120+
121+
return true;
122+
}
123+
124+
private static boolean isAlphanumeric(char c) {
125+
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
126+
}
127+
128+
private static boolean isLowerCaseAlphanumeric(char c) {
129+
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
80130
}
81131

82132
// AWS related functions
@@ -126,13 +176,108 @@ public class RulesFunctions {
126176
return values.get(index);
127177
}
128178

129-
public static boolean awsIsVirtualHostableS3Bucket(String hostLabel, boolean allowSubDomains) {
130-
if (allowSubDomains) {
131-
return VIRTUAL_HOSTABLE_BUCKET.matcher(hostLabel).matches()
132-
// don't allow ip address
133-
&& !NO_IPS.matcher(hostLabel).matches() && !NO_CONSECUTIVE_DASH_OR_DOTS.matcher(hostLabel).matches();
179+
public static boolean awsIsVirtualHostableS3Bucket(String hostLabel, boolean allowDots) {
180+
// Bucket names must be between 3 (min) and 63 (max) characters long.
181+
int bucketLength = hostLabel == null ? 0 : hostLabel.length();
182+
if (bucketLength < MIN_BUCKET_SIZE || bucketLength > MAX_HOST_LABEL_SIZE) {
183+
return false;
184+
}
185+
186+
// Bucket names must begin and end with a letter or number.
187+
if (!isLowerCaseAlphanumeric(hostLabel.charAt(0)) || !isLowerCaseAlphanumeric(hostLabel.charAt(bucketLength - 1))) {
188+
return false;
189+
}
190+
191+
// Bucket names can consist only of lowercase letters, numbers, periods (.), and hyphens (-).
192+
if (!allowDots) {
193+
for (int i = 1; i < bucketLength - 1; i++) { // already validated 0 and N - 1.
194+
if (!isValidBucketSegmentChar(hostLabel.charAt(i))) {
195+
return false;
196+
}
197+
}
198+
return true;
199+
}
200+
201+
// Check for consecutive dots or hyphens
202+
char last = hostLabel.charAt(0);
203+
for (int i = 1; i < bucketLength; i++) {
204+
char c = hostLabel.charAt(i);
205+
// Don't allow "bucket-.foo" or "bucket.-foo"
206+
if (c == '.') {
207+
if (last == '.' || last == '-') {
208+
return false;
209+
}
210+
} else if (c == '-') {
211+
if (last == '.') {
212+
return false;
213+
}
214+
} else if (!isLowerCaseAlphanumeric(c)) {
215+
return false;
216+
}
217+
last = c;
218+
}
219+
220+
// Bucket names must not be formatted as an IP address (for example, 192.168.5.4).
221+
return !isIpAddr(hostLabel);
222+
}
223+
224+
private static boolean isValidBucketSegmentChar(char c) {
225+
return isLowerCaseAlphanumeric(c) || c == '-';
226+
}
227+
228+
private static boolean isIpAddr(String host) {
229+
if (host == null || host.length() < 2) {
230+
return false;
231+
}
232+
233+
// Simple check for IPv6 (enclosed in square brackets)
234+
if (host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
235+
return true;
236+
}
237+
238+
int from = 0;
239+
int segments = 0;
240+
boolean done = false;
241+
while (!done) {
242+
int index = host.indexOf('.', from);
243+
if (index == -1) {
244+
if (segments != 3) {
245+
// E.g., 123.com
246+
return false;
247+
}
248+
index = host.length();
249+
done = true;
250+
} else if (segments == 3) {
251+
// E.g., 1.2.3.4.5
252+
return false;
253+
}
254+
int length = index - from;
255+
if (length == 1) {
256+
char ch0 = host.charAt(from);
257+
if (ch0 < '0' || ch0 > '9') {
258+
return false;
259+
}
260+
} else if (length == 2) {
261+
char ch0 = host.charAt(from);
262+
char ch1 = host.charAt(from + 1);
263+
if ((ch0 <= '0' || ch0 > '9') || (ch1 < '0' || ch1 > '9')) {
264+
return false;
265+
}
266+
} else if (length == 3) {
267+
char ch0 = host.charAt(from);
268+
char ch1 = host.charAt(from + 1);
269+
char ch2 = host.charAt(from + 2);
270+
if ((ch0 <= '0' || ch0 > '9') || (ch1 < '0' || ch1 > '9') || (ch2 < '0' || ch2 > '9')) {
271+
return false;
272+
}
273+
// This is a heuristic; We are intentionally not checking for the range 000-255.
274+
} else {
275+
return false;
276+
}
277+
from = index + 1;
278+
segments += 1;
134279
}
135-
return VIRTUAL_HOSTABLE_BUCKET_NO_SUBDOMAINS.matcher(hostLabel).matches();
280+
return true;
136281
}
137282

138283
private static PartitionData loadPartitionData() {

0 commit comments

Comments
 (0)