Skip to content

Commit 3b634fc

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 3b634fc

2 files changed

Lines changed: 91 additions & 44 deletions

File tree

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

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,62 +37,84 @@ 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 fully
43+
* consumed as opaque tokens, so a {@code charset}-looking substring inside another parameter's
44+
* quoted value (e.g. {@code boundary="charset=oops"}) can never be mistaken for a real {@code
45+
* charset} parameter. If a {@code charset} parameter is found but its value is not a valid
46+
* charset name, the search continues to any later {@code charset} parameter instead of giving up.
47+
*/
4048
public static Charset extractCharset(String contentType) {
4149
if (contentType == null) return null;
4250
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++) {
51+
// Skip the media type itself (e.g. "text/plain") up to the first parameter delimiter.
52+
int i = skipToDelimiter(contentType, 0, len);
53+
while (i < len) {
54+
i++; // consume the ';' or ',' that ends the previous token
55+
i = skipOws(contentType, i, len);
56+
int nameStart = i;
57+
while (i < len
58+
&& contentType.charAt(i) != '='
59+
&& contentType.charAt(i) != ';'
60+
&& contentType.charAt(i) != ',') {
61+
i++;
62+
}
63+
if (i >= len || contentType.charAt(i) != '=') {
64+
// Parameter with no '=' (malformed) — skip past it and keep looking.
65+
i = skipToDelimiter(contentType, i, len);
66+
continue;
67+
}
68+
int nameEnd = i;
69+
while (nameEnd > nameStart && isOws(contentType.charAt(nameEnd - 1))) nameEnd--;
70+
String name = contentType.substring(nameStart, nameEnd);
71+
i = skipOws(contentType, i + 1, len);
72+
String value;
73+
if (i < len && contentType.charAt(i) == '"') {
74+
StringBuilder sb = new StringBuilder();
75+
i++;
76+
while (i < len && contentType.charAt(i) != '"') {
6377
char c = contentType.charAt(i);
64-
if (c == ';' || c == ',' || c == ' ') {
65-
end = i;
66-
break;
78+
if (c == '\\' && i + 1 < len) {
79+
i++;
80+
c = contentType.charAt(i);
6781
}
82+
sb.append(c);
83+
i++;
6884
}
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-
}
85+
if (i < len) i++; // consume closing quote
86+
value = sb.toString();
87+
i = skipToDelimiter(contentType, i, len);
88+
} else {
89+
int valStart = i;
90+
i = skipToDelimiter(contentType, i, len);
91+
int valEnd = i;
92+
while (valEnd > valStart && isOws(contentType.charAt(valEnd - 1))) valEnd--;
93+
value = contentType.substring(valStart, valEnd);
94+
}
95+
if (name.equalsIgnoreCase("charset")) {
7396
try {
74-
return Charset.forName(name);
75-
} catch (IllegalArgumentException e) {
76-
return null;
97+
return Charset.forName(value);
98+
} catch (IllegalArgumentException ignored) {
99+
// An invalid charset value here does not rule out a later, valid charset parameter.
77100
}
78101
}
79-
searchFrom = idx + 1;
80102
}
103+
return null;
81104
}
82105

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;
106+
private static int skipToDelimiter(String s, int i, int len) {
107+
while (i < len && s.charAt(i) != ';' && s.charAt(i) != ',') i++;
108+
return i;
109+
}
110+
111+
private static boolean isOws(char c) {
112+
return c == ' ' || c == '\t';
113+
}
114+
115+
private static int skipOws(String s, int i, int len) {
116+
while (i < len && isOws(s.charAt(i))) i++;
117+
return i;
96118
}
97119

98120
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)