fix percent decoding when parameter is query domain

This commit is contained in:
Jörg Prante 2023-12-07 17:26:48 +01:00
parent 3a4a3c692f
commit 183041acd3
9 changed files with 78 additions and 17 deletions

View file

@ -1,5 +1,5 @@
group = org.xbib group = org.xbib
name = net name = net
version = 4.0.0 version = 4.0.1
org.gradle.warning.mode = ALL org.gradle.warning.mode = ALL

View file

@ -4,34 +4,39 @@ import org.xbib.net.Parameter;
import org.xbib.net.ParameterBuilder; import org.xbib.net.ParameterBuilder;
import org.xbib.net.PathNormalizer; import org.xbib.net.PathNormalizer;
import java.nio.charset.Charset;
public class PathDecoder { public class PathDecoder {
private final String path; private final String path;
private final String query; private final String query;
private final Charset charset;
private final ParameterBuilder params; private final ParameterBuilder params;
public PathDecoder(String pathAndQuery) { public PathDecoder(String pathAndQuery, Charset charset) {
this(pathAndQuery, null); this(pathAndQuery, null, charset);
} }
public PathDecoder(String pathAndQuery, String queryString) { public PathDecoder(String pathAndQuery, String queryString, Charset charset) {
this.charset = charset;
int pos = pathAndQuery.indexOf('?'); int pos = pathAndQuery.indexOf('?');
String path = pos > 0 ? pathAndQuery.substring(0, pos) : pathAndQuery; String path = pos > 0 ? pathAndQuery.substring(0, pos) : pathAndQuery;
this.query = pos > 0 ? pathAndQuery.substring(pos + 1) : null; this.query = pos > 0 ? pathAndQuery.substring(pos + 1) : null;
this.path = PathNormalizer.normalize(path); this.path = PathNormalizer.normalize(path);
this.params = Parameter.builder().domain(Parameter.Domain.PATH).enablePercentDecoding(); this.params = Parameter.builder().domain(Parameter.Domain.PATH).enablePercentDecoding();
if (query != null) { if (query != null) {
this.params.add(query); this.params.add(query, charset);
} }
if (queryString != null) { if (queryString != null) {
this.params.add(queryString); this.params.add(queryString, charset);
} }
} }
public void parse(String queryString) { public void parse(String queryString) {
this.params.add(queryString); this.params.add(queryString, charset);
} }
public String path() { public String path() {

View file

@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import org.xbib.net.Parameter; import org.xbib.net.Parameter;
import org.xbib.net.URL; import org.xbib.net.URL;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
@ -11,27 +13,27 @@ class PathDecoderTest {
@Test @Test
void testPlusSign() throws Exception { void testPlusSign() throws Exception {
PathDecoder decoder = new PathDecoder("/path?a=b+c", "d=e+f"); PathDecoder decoder = new PathDecoder("/path?a=b+c", "d=e+f", StandardCharsets.UTF_8);
assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString()); assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString());
assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString()); assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString());
} }
@Test @Test
void testSlash() throws Exception { void testSlash() throws Exception {
PathDecoder decoder = new PathDecoder("path/foo/bar/?a=b+c", "d=e+f"); PathDecoder decoder = new PathDecoder("path/foo/bar/?a=b+c", "d=e+f", StandardCharsets.UTF_8);
assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString()); assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString());
assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString()); assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString());
} }
@Test @Test
void testDoubleSlashes() { void testDoubleSlashes() {
PathDecoder decoder = new PathDecoder("//path", ""); PathDecoder decoder = new PathDecoder("//path", "", StandardCharsets.UTF_8);
assertEquals("/path", decoder.path()); assertEquals("/path", decoder.path());
} }
@Test @Test
void testSlashes() throws Exception { void testSlashes() throws Exception {
PathDecoder decoder = new PathDecoder("//path?a=b+c", "d=e+f"); PathDecoder decoder = new PathDecoder("//path?a=b+c", "d=e+f", StandardCharsets.UTF_8);
assertEquals("/path", decoder.path()); assertEquals("/path", decoder.path());
assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString()); assertEquals("[b c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString());
assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString()); assertEquals("[e f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString());
@ -39,7 +41,7 @@ class PathDecoderTest {
@Test @Test
void testPlusPercent() throws Exception { void testPlusPercent() throws Exception {
PathDecoder decoder = new PathDecoder("//path?a=b%2Bc", "d=e%2Bf"); PathDecoder decoder = new PathDecoder("//path?a=b%2Bc", "d=e%2Bf", StandardCharsets.UTF_8);
assertEquals("/path", decoder.path()); assertEquals("/path", decoder.path());
assertEquals("[b+c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString()); assertEquals("[b+c]", decoder.getParameter().getAll("a", Parameter.Domain.PATH).toString());
assertEquals("[e+f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString()); assertEquals("[e+f]", decoder.getParameter().getAll("d", Parameter.Domain.PATH).toString());
@ -53,7 +55,7 @@ class PathDecoderTest {
assertNull(url.getPort()); assertNull(url.getPort());
assertEquals("/pdfconverter/index.gtpl", url.getPath()); assertEquals("/pdfconverter/index.gtpl", url.getPath());
assertNull(url.getFragment()); assertNull(url.getFragment());
PathDecoder decoder = new PathDecoder(requestURI); PathDecoder decoder = new PathDecoder(requestURI, StandardCharsets.UTF_8);
if (url.getQuery() != null) { if (url.getQuery() != null) {
decoder.parse(url.getDecodedQuery()); decoder.parse(url.getDecodedQuery());
} }

View file

@ -1,5 +1,7 @@
package org.xbib.net; package org.xbib.net;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -102,9 +104,17 @@ public class Parameter implements Iterable<Pair<String, Object>>, Comparable<Par
} else { } else {
object = null; object = null;
} }
return object != null ? object.toString() : null; return object != null ? decodeIfQueryDomain(domain, object.toString()) : null;
}
return object != null ? decodeIfQueryDomain(domain, object instanceof String ? (String) object : object.toString()) : null;
}
private String decodeIfQueryDomain(Domain domain, String string) throws ParameterException {
try {
return Domain.QUERY == domain ? builder().percentDecode(string) : string;
} catch (UnmappableCharacterException | MalformedInputException e) {
throw new ParameterException(e);
} }
return object != null ? object instanceof String ? (String) object : object.toString() : null;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

@ -14,6 +14,8 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.xbib.datastructures.common.ImmutableList; import org.xbib.datastructures.common.ImmutableList;
import org.xbib.datastructures.common.MultiMap; import org.xbib.datastructures.common.MultiMap;
import org.xbib.datastructures.common.Pair; import org.xbib.datastructures.common.Pair;
@ -107,11 +109,25 @@ public class ParameterBuilder implements PairValidator {
return this; return this;
} }
public String percentEncode(CharSequence text) throws UnmappableCharacterException, MalformedInputException {
if (percentEncoder == null) {
charset(StandardCharsets.UTF_8);
}
return percentEncoder.encode(text);
}
public ParameterBuilder percentDecode(PercentDecoder percentDecoder) { public ParameterBuilder percentDecode(PercentDecoder percentDecoder) {
this.percentDecoder = percentDecoder; this.percentDecoder = percentDecoder;
return this; return this;
} }
public String percentDecode(CharSequence text) throws UnmappableCharacterException, MalformedInputException {
if (percentDecoder == null) {
charset(StandardCharsets.UTF_8);
}
return percentDecoder.decode(text);
}
public ParameterBuilder enablePercentEncoding() { public ParameterBuilder enablePercentEncoding() {
this.enablePercentEncoding = true; this.enablePercentEncoding = true;
charset(StandardCharsets.UTF_8); charset(StandardCharsets.UTF_8);
@ -222,7 +238,11 @@ public class ParameterBuilder implements PairValidator {
return this; return this;
} }
public ParameterBuilder add(String percentEncodedQueryString) { public ParameterBuilder add(String percentEncodedQueryString, Charset charset) {
Objects.requireNonNull(charset);
if (this.charset == null || !this.charset.equals(charset)) {
charset(charset);
}
try { try {
decodeQueryString(percentEncodedQueryString); decodeQueryString(percentEncodedQueryString);
} catch (MalformedInputException | UnmappableCharacterException e) { } catch (MalformedInputException | UnmappableCharacterException e) {
@ -235,6 +255,9 @@ public class ParameterBuilder implements PairValidator {
if (enableSort) { if (enableSort) {
list.sort(Comparator.comparing(Pair::getKey)); list.sort(Comparator.comparing(Pair::getKey));
} }
if (percentDecoder == null) {
charset(StandardCharsets.UTF_8);
}
String queryString = null; String queryString = null;
if (enableQueryString) { if (enableQueryString) {
try { try {

View file

@ -3,6 +3,10 @@ package org.xbib.net;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ParameterException extends Exception { public class ParameterException extends Exception {
public ParameterException(Exception e) {
super(e);
}
public ParameterException(String message) { public ParameterException(String message) {
super(message); super(message);
} }

View file

@ -77,6 +77,17 @@ public class ParameterTest {
parameter.getAsQueryString()); parameter.getAsQueryString());
} }
@Test
public void testQueryParameterDecoding() throws ParameterException {
Parameter parameter = Parameter.builder()
.domain(Parameter.Domain.QUERY)
.add("value1", "Hello%20J%C3%B6rg") // query parameter, no query string mode
.add("value2=Hello%20J%C3%B6rg", StandardCharsets.UTF_8) // query string mode
.build();
assertEquals("Hello Jörg", parameter.getAsString("value1", Parameter.Domain.QUERY));
assertEquals("Hello Jörg", parameter.getAsString("value2", Parameter.Domain.QUERY));
}
@Test @Test
public void testParameters() { public void testParameters() {
Map<String, Object> map = Map.of( Map<String, Object> map = Map.of(

View file

@ -56,6 +56,11 @@ class PercentDecoderTest {
}); });
} }
@Test
void testJoerg() throws UnmappableCharacterException, MalformedInputException {
assertEquals("Hello Jörg", decoder.decode("Hello%20J%C3%B6rg"));
}
@Test @Test
void testRandomStrings() throws MalformedInputException, UnmappableCharacterException { void testRandomStrings() throws MalformedInputException, UnmappableCharacterException {
PercentEncoder encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); PercentEncoder encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);

View file

@ -3,6 +3,7 @@ package org.xbib.net;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction; import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -498,7 +499,7 @@ class URLParserTest {
private void assertUrlCompatibility(String url) throws Exception { private void assertUrlCompatibility(String url) throws Exception {
String s = URL.from(url).toExternalForm(); String s = URL.from(url).toExternalForm();
assertEquals(s, URL.from(s).toExternalForm()); assertEquals(s, URL.from(s).toExternalForm());
assertEquals(s, new java.net.URL(url).toExternalForm()); assertEquals(s, URI.create(url).toURL().toExternalForm());
} }
private void assertRoundTrip(String url) { private void assertRoundTrip(String url) {