Support semicolons in query parameters as explain in the W3C recommentation (#9701)

Motivation:

Support semicolons in query parameters as explain in the W3C recommentation:
https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data

Modification:

- Add a new constructor arg that can be used to "switch" modes for decoding ;
- Add unit test

Result:

Fixes #8855
This commit is contained in:
Norman Maurer 2019-10-23 23:44:37 -07:00
parent 03ad809a3b
commit af132384cc
2 changed files with 43 additions and 5 deletions

View File

@ -69,6 +69,7 @@ public class QueryStringDecoder {
private final Charset charset; private final Charset charset;
private final String uri; private final String uri;
private final int maxParams; private final int maxParams;
private final boolean semicolonIsNormalChar;
private int pathEndIdx; private int pathEndIdx;
private String path; private String path;
private Map<String, List<String>> params; private Map<String, List<String>> params;
@ -110,9 +111,19 @@ public class QueryStringDecoder {
* specified charset. * specified charset.
*/ */
public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) { public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) {
this(uri, charset, hasPath, maxParams, false);
}
/**
* Creates a new decoder that decodes the specified URI encoded in the
* specified charset.
*/
public QueryStringDecoder(String uri, Charset charset, boolean hasPath,
int maxParams, boolean semicolonIsNormalChar) {
this.uri = requireNonNull(uri, "uri"); this.uri = requireNonNull(uri, "uri");
this.charset = requireNonNull(charset, "charset"); this.charset = requireNonNull(charset, "charset");
this.maxParams = checkPositive(maxParams, "maxParams"); this.maxParams = checkPositive(maxParams, "maxParams");
this.semicolonIsNormalChar = semicolonIsNormalChar;
// `-1` means that path end index will be initialized lazily // `-1` means that path end index will be initialized lazily
pathEndIdx = hasPath ? -1 : 0; pathEndIdx = hasPath ? -1 : 0;
@ -139,6 +150,14 @@ public class QueryStringDecoder {
* specified charset. * specified charset.
*/ */
public QueryStringDecoder(URI uri, Charset charset, int maxParams) { public QueryStringDecoder(URI uri, Charset charset, int maxParams) {
this(uri, charset, maxParams, false);
}
/**
* Creates a new decoder that decodes the specified URI encoded in the
* specified charset.
*/
public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) {
String rawPath = uri.getRawPath(); String rawPath = uri.getRawPath();
if (rawPath == null) { if (rawPath == null) {
rawPath = EMPTY_STRING; rawPath = EMPTY_STRING;
@ -148,6 +167,7 @@ public class QueryStringDecoder {
this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery; this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery;
this.charset = requireNonNull(charset, "charset"); this.charset = requireNonNull(charset, "charset");
this.maxParams = checkPositive(maxParams, "maxParams"); this.maxParams = checkPositive(maxParams, "maxParams");
this.semicolonIsNormalChar = semicolonIsNormalChar;
pathEndIdx = rawPath.length(); pathEndIdx = rawPath.length();
} }
@ -178,7 +198,7 @@ public class QueryStringDecoder {
*/ */
public Map<String, List<String>> parameters() { public Map<String, List<String>> parameters() {
if (params == null) { if (params == null) {
params = decodeParams(uri, pathEndIdx(), charset, maxParams); params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar);
} }
return params; return params;
} }
@ -205,7 +225,8 @@ public class QueryStringDecoder {
return pathEndIdx; return pathEndIdx;
} }
private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit) { private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit,
boolean semicolonIsNormalChar) {
int len = s.length(); int len = s.length();
if (from >= len) { if (from >= len) {
return Collections.emptyMap(); return Collections.emptyMap();
@ -227,8 +248,12 @@ public class QueryStringDecoder {
valueStart = i + 1; valueStart = i + 1;
} }
break; break;
case '&':
case ';': case ';':
if (semicolonIsNormalChar) {
continue;
}
// fall-through
case '&':
if (addParam(s, nameStart, valueStart, i, params, charset)) { if (addParam(s, nameStart, valueStart, i, params, charset)) {
paramsLimit--; paramsLimit--;
if (paramsLimit == 0) { if (paramsLimit == 0) {

View File

@ -129,6 +129,13 @@ public class QueryStringDecoderTest {
assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a="); assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a=");
} }
@Test
public void testSemicolon() {
assertQueryString("/foo?a=1;2", "/foo?a=1;2", false);
// ";" should be treated as a normal character, see #8855
assertQueryString("/foo?a=1;2", "/foo?a=1%3B2", true);
}
@Test @Test
public void testPathSpecific() { public void testPathSpecific() {
// decode escaped characters // decode escaped characters
@ -225,8 +232,14 @@ public class QueryStringDecoderTest {
} }
private static void assertQueryString(String expected, String actual) { private static void assertQueryString(String expected, String actual) {
QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8); assertQueryString(expected, actual, false);
QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8); }
private static void assertQueryString(String expected, String actual, boolean semicolonIsNormalChar) {
QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8, true,
1024, semicolonIsNormalChar);
QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8, true,
1024, semicolonIsNormalChar);
Assert.assertEquals(ed.path(), ad.path()); Assert.assertEquals(ed.path(), ad.path());
Assert.assertEquals(ed.parameters(), ad.parameters()); Assert.assertEquals(ed.parameters(), ad.parameters());
} }