Skip to content

Commit 57589a2

Browse files
committed
internal-api: rewrite extractCharset as a quoted-aware parameter tokenizer
Replace the index-based substring search with a proper media-type parameter walk: quoted parameter values are consumed as opaque tokens (so a "charset"-looking substring inside another parameter's quoted value can never be mistaken for a real charset parameter), and an invalid charset value no longer aborts the search: a later, valid charset parameter is still found.
1 parent fcb7cdc commit 57589a2

2 files changed

Lines changed: 92 additions & 44 deletions

File tree

internal-api/src/main/java/datadog/trace/api/http/MultipartContentDecoder.java

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,62 +37,85 @@ public static String decodeBytes(byte[] buf, int length, String contentType) {
3737
}
3838
}
3939

40+
/**
41+
* Walks the media-type parameter list of a Content-Type value looking for a {@code charset}
42+
* parameter, honoring RFC 7230 quoted-string syntax for parameter values. Quoted values are
43+
* fully consumed as opaque tokens, so a {@code charset}-looking substring inside another
44+
* parameter's quoted value (e.g. {@code boundary="charset=oops"}) can never be mistaken for a
45+
* real {@code charset} parameter. If a {@code charset} parameter is found but its value is not a
46+
* valid charset name, the search continues to any later {@code charset} parameter instead of
47+
* giving up.
48+
*/
4049
public static Charset extractCharset(String contentType) {
4150
if (contentType == null) return null;
4251
int len = contentType.length();
43-
int searchFrom = 0;
44-
while (true) {
45-
int idx = indexOfIgnoreAsciiCase(contentType, "charset", searchFrom);
46-
if (idx < 0) return null;
47-
// Require a parameter boundary before "charset" so "xcharset=..." is not matched.
48-
// RFC 7230 optional whitespace (OWS) allows both space and horizontal tab.
49-
boolean boundaryOk =
50-
idx == 0
51-
|| contentType.charAt(idx - 1) == ';'
52-
|| contentType.charAt(idx - 1) == ' '
53-
|| contentType.charAt(idx - 1) == '\t';
54-
// Also allow OWS around the "=", e.g. "charset = ISO-8859-1".
55-
int j = idx + 7;
56-
while (j < len && (contentType.charAt(j) == ' ' || contentType.charAt(j) == '\t')) j++;
57-
if (boundaryOk && j < len && contentType.charAt(j) == '=') {
58-
j++;
59-
while (j < len && (contentType.charAt(j) == ' ' || contentType.charAt(j) == '\t')) j++;
60-
int nameStart = j;
61-
int end = len;
62-
for (int i = nameStart; i < end; i++) {
52+
// Skip the media type itself (e.g. "text/plain") up to the first parameter delimiter.
53+
int i = skipToDelimiter(contentType, 0, len);
54+
while (i < len) {
55+
i++; // consume the ';' or ',' that ends the previous token
56+
i = skipOws(contentType, i, len);
57+
int nameStart = i;
58+
while (i < len
59+
&& contentType.charAt(i) != '='
60+
&& contentType.charAt(i) != ';'
61+
&& contentType.charAt(i) != ',') {
62+
i++;
63+
}
64+
if (i >= len || contentType.charAt(i) != '=') {
65+
// Parameter with no '=' (malformed) — skip past it and keep looking.
66+
i = skipToDelimiter(contentType, i, len);
67+
continue;
68+
}
69+
int nameEnd = i;
70+
while (nameEnd > nameStart && isOws(contentType.charAt(nameEnd - 1))) nameEnd--;
71+
String name = contentType.substring(nameStart, nameEnd);
72+
i = skipOws(contentType, i + 1, len);
73+
String value;
74+
if (i < len && contentType.charAt(i) == '"') {
75+
StringBuilder sb = new StringBuilder();
76+
i++;
77+
while (i < len && contentType.charAt(i) != '"') {
6378
char c = contentType.charAt(i);
64-
if (c == ';' || c == ',' || c == ' ') {
65-
end = i;
66-
break;
79+
if (c == '\\' && i + 1 < len) {
80+
i++;
81+
c = contentType.charAt(i);
6782
}
83+
sb.append(c);
84+
i++;
6885
}
69-
String name = contentType.substring(nameStart, end).trim();
70-
if (name.length() > 1 && name.charAt(0) == '"' && name.charAt(name.length() - 1) == '"') {
71-
name = name.substring(1, name.length() - 1);
72-
}
86+
if (i < len) i++; // consume closing quote
87+
value = sb.toString();
88+
i = skipToDelimiter(contentType, i, len);
89+
} else {
90+
int valStart = i;
91+
i = skipToDelimiter(contentType, i, len);
92+
int valEnd = i;
93+
while (valEnd > valStart && isOws(contentType.charAt(valEnd - 1))) valEnd--;
94+
value = contentType.substring(valStart, valEnd);
95+
}
96+
if (name.equalsIgnoreCase("charset")) {
7397
try {
74-
return Charset.forName(name);
75-
} catch (IllegalArgumentException e) {
76-
return null;
98+
return Charset.forName(value);
99+
} catch (IllegalArgumentException ignored) {
100+
// An invalid charset value here does not rule out a later, valid charset parameter.
77101
}
78102
}
79-
searchFrom = idx + 1;
80103
}
104+
return null;
81105
}
82106

83-
private static int indexOfIgnoreAsciiCase(String s, String needle, int fromIndex) {
84-
int sLen = s.length();
85-
int nLen = needle.length();
86-
outer:
87-
for (int i = fromIndex, max = sLen - nLen; i <= max; i++) {
88-
for (int j = 0; j < nLen; j++) {
89-
if (Character.toLowerCase(s.charAt(i + j)) != needle.charAt(j)) {
90-
continue outer;
91-
}
92-
}
93-
return i;
94-
}
95-
return -1;
107+
private static int skipToDelimiter(String s, int i, int len) {
108+
while (i < len && s.charAt(i) != ';' && s.charAt(i) != ',') i++;
109+
return i;
110+
}
111+
112+
private static boolean isOws(char c) {
113+
return c == ' ' || c == '\t';
114+
}
115+
116+
private static int skipOws(String s, int i, int len) {
117+
while (i < len && isOws(s.charAt(i))) i++;
118+
return i;
96119
}
97120

98121
private MultipartContentDecoder() {}

internal-api/src/test/java/datadog/trace/api/http/MultipartContentDecoderTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,31 @@ void extractCharsetAcceptsWhitespaceAroundEqualsSign(String contentType) {
134134
assertEquals("ISO-8859-1", MultipartContentDecoder.extractCharset(contentType).name());
135135
}
136136

137+
@Test
138+
void extractCharsetContinuesSearchAfterInvalidCharsetValue() {
139+
// The first "charset" parameter is invalid; a later, valid one must still be found.
140+
assertEquals(
141+
"UTF-8",
142+
MultipartContentDecoder.extractCharset("text/plain; charset=NOTACHARSET; charset=UTF-8")
143+
.name());
144+
}
145+
146+
@Test
147+
void extractCharsetIgnoresCharsetLookingSubstringInsideQuotedParameterValue() {
148+
// The quoted boundary value contains "charset=" but must be treated as opaque text, not a
149+
// real charset parameter; the actual charset= that follows must be used.
150+
assertEquals(
151+
"UTF-8",
152+
MultipartContentDecoder.extractCharset(
153+
"text/plain; boundary=\"charset=oops\"; charset=UTF-8")
154+
.name());
155+
}
156+
157+
@Test
158+
void extractCharsetReturnsNullWhenOnlyMatchIsInsideQuotedParameterValue() {
159+
assertNull(MultipartContentDecoder.extractCharset("text/plain; boundary=\"charset=oops\""));
160+
}
161+
137162
@Test
138163
void extractCharsetIgnoresSubstringMatchInParameterName() {
139164
// "xcharset=UTF-16" must not match; the real "charset=UTF-8" that follows must be used

0 commit comments

Comments
 (0)