Skip to content

Commit ff33d15

Browse files
committed
refactor: Reduce cognitive complexity in path validation; drop dead IT comments
1 parent a8fd01b commit ff33d15

3 files changed

Lines changed: 50 additions & 50 deletions

File tree

src/main/java/com/retailsvc/http/internal/ExtrasPathValidator.java

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ private ExtrasPathValidator() {}
1313

1414
public static String validateAndDecode(URI uri) {
1515
String raw = uri.getRawPath();
16+
validateRaw(raw);
17+
18+
String decoded = uri.getPath();
19+
if (decoded == null) {
20+
throw new BadRequestException("missing path");
21+
}
22+
rejectControlChars(decoded, "decoded path contains control character");
23+
validateSegments(decoded);
24+
return decoded;
25+
}
26+
27+
private static void validateRaw(String raw) {
1628
if (raw == null) {
1729
throw new BadRequestException("missing path");
1830
}
@@ -22,25 +34,19 @@ public static String validateAndDecode(URI uri) {
2234
if (raw.indexOf('\\') >= 0) {
2335
throw new BadRequestException("path contains backslash");
2436
}
25-
for (int i = 0; i < raw.length(); i++) {
26-
char c = raw.charAt(i);
27-
if (c < 0x20 || c == 0x7f) {
28-
throw new BadRequestException("path contains control character");
29-
}
30-
}
31-
32-
String decoded = uri.getPath();
33-
if (decoded == null) {
34-
throw new BadRequestException("missing path");
35-
}
37+
rejectControlChars(raw, "path contains control character");
38+
}
3639

37-
for (int i = 0; i < decoded.length(); i++) {
38-
char c = decoded.charAt(i);
40+
private static void rejectControlChars(String s, String message) {
41+
for (int i = 0; i < s.length(); i++) {
42+
char c = s.charAt(i);
3943
if (c < 0x20 || c == 0x7f) {
40-
throw new BadRequestException("decoded path contains control character");
44+
throw new BadRequestException(message);
4145
}
4246
}
47+
}
4348

49+
private static void validateSegments(String decoded) {
4450
String[] segments = decoded.substring(decoded.startsWith("/") ? 1 : 0).split("/", -1);
4551
for (int i = 0; i < segments.length; i++) {
4652
String s = segments[i];
@@ -51,7 +57,5 @@ public static String validateAndDecode(URI uri) {
5157
throw new BadRequestException("path traversal segment");
5258
}
5359
}
54-
55-
return decoded;
5660
}
5761
}

src/main/java/com/retailsvc/http/internal/PathPattern.java

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,41 +24,41 @@ public static PathPattern compile(String raw) {
2424
String prev = null;
2525
for (int i = 0; i < segments.length; i++) {
2626
String seg = segments[i];
27-
if (seg.isEmpty()
28-
&& !(i == segments.length - 1 && segments.length > 1 && raw.endsWith("/"))) {
29-
throw new IllegalArgumentException("empty segment in path: " + raw);
30-
}
31-
if (seg.contains("*") && !seg.equals("*") && !seg.equals("**")) {
32-
throw new IllegalArgumentException(
33-
"'*' and '**' must be a whole segment, not " + seg + " in " + raw);
34-
}
35-
if ("**".equals(seg) && "**".equals(prev)) {
36-
throw new IllegalArgumentException("adjacent '**' segments in " + raw);
37-
}
27+
validateSegment(seg, prev, i, segments.length, raw);
3828
boolean trailing = i == segments.length - 1;
39-
switch (seg) {
40-
case "*" -> {
41-
rx.append("/[^/]+");
42-
hasWildcard = true;
43-
}
44-
case "**" -> {
45-
if (trailing) {
46-
// Slash is required; anything (including empty string) may follow it.
47-
rx.append("/.*");
48-
} else {
49-
// At least one character and a slash must appear before the next segment.
50-
rx.append("/.+");
51-
}
52-
hasWildcard = true;
53-
}
54-
default -> rx.append("/").append(Pattern.quote(seg));
55-
}
29+
hasWildcard |= appendSegment(rx, seg, trailing);
5630
prev = seg;
5731
}
5832
rx.append("$");
5933
return new PathPattern(raw, Pattern.compile(rx.toString()), hasWildcard);
6034
}
6135

36+
private static void validateSegment(String seg, String prev, int i, int total, String raw) {
37+
boolean trailingEmptyAllowed = i == total - 1 && total > 1 && raw.endsWith("/");
38+
if (seg.isEmpty() && !trailingEmptyAllowed) {
39+
throw new IllegalArgumentException("empty segment in path: " + raw);
40+
}
41+
if (seg.contains("*") && !seg.equals("*") && !seg.equals("**")) {
42+
throw new IllegalArgumentException(
43+
"'*' and '**' must be a whole segment, not " + seg + " in " + raw);
44+
}
45+
if ("**".equals(seg) && "**".equals(prev)) {
46+
throw new IllegalArgumentException("adjacent '**' segments in " + raw);
47+
}
48+
}
49+
50+
private static boolean appendSegment(StringBuilder rx, String seg, boolean trailing) {
51+
switch (seg) {
52+
case "*" -> rx.append("/[^/]+");
53+
case "**" -> rx.append(trailing ? "/.*" : "/.+");
54+
default -> {
55+
rx.append("/").append(Pattern.quote(seg));
56+
return false;
57+
}
58+
}
59+
return true;
60+
}
61+
6262
public boolean hasWildcard() {
6363
return wildcard;
6464
}

src/test/java/com/retailsvc/http/ExtrasWildcardIT.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,8 @@ void traversalReturns400() throws Exception {
107107
assertThat(get(client, s, "/files/%2fetc").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
108108
// %5c is a backslash — reject encoded backslashes
109109
assertThat(get(client, s, "/files/x%5cy").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
110-
// %00 is a null byte — java.net.URI rejects raw NUL in the path; defense at the
111-
// router is still valid for clients that bypass URI parsing, but we cannot express
112-
// this vector via java.net.http.HttpClient (URI.create throws before the wire).
113-
// assertThat(get(client, s, "/files/x%00").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
114-
// %0a is a line-feed — same reason as %00: JDK URI rejects it pre-wire.
115-
// assertThat(get(client, s, "/files/x%0ay").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
110+
// %00 (NUL) and %0a (LF) cannot be tested here: java.net.URI rejects them before
111+
// they reach the wire. Router-level defence is exercised by ExtrasPathValidatorTest.
116112
assertThat(get(client, s, "/files//x").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
117113
assertThat(get(client, s, "/files/.").statusCode()).isEqualTo(HTTP_BAD_REQUEST);
118114
assertThat(get(client, s, "/files/./x").statusCode()).isEqualTo(HTTP_BAD_REQUEST);

0 commit comments

Comments
 (0)