PathPatternParser encodes patterns as it parses them
Before this commit there was no special handling for URL encoding of the path pattern string coming into the path pattern parser. No assumptions were made about it being in an encoded form or not. With this change it is assumed incoming path patterns are not encoded and as part of parsing the parser builds PathPattern objects that include encoded elements. For example parsing "/f o" will create a path pattern of the form "/f%20o". In this form it can then be used to match against encoded paths. Handling encoded characters is not trivial and has resulted in some loss in matching speed but care has been taken to avoid unnecessary creation of additional heap objects. When matching variables the variable values are return in a decoded form. It is hoped the speed can be recovered, at least for the common case of non-encoded incoming paths. Issue: SPR-15640
This commit is contained in:
parent
c0550f7eb6
commit
ff2af660cf
|
|
@ -56,8 +56,8 @@ class CaptureTheRestPathElement extends PathElement {
|
|||
matchingContext.remainingPathIndex = matchingContext.candidateLength;
|
||||
}
|
||||
if (matchingContext.extractingVariables) {
|
||||
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex,
|
||||
matchingContext.candidateLength - candidateIndex));
|
||||
matchingContext.set(variableName, decode(new String(matchingContext.candidate, candidateIndex,
|
||||
matchingContext.candidateLength - candidateIndex)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@
|
|||
|
||||
package org.springframework.web.util.pattern;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
/**
|
||||
* A path element representing capturing a piece of the path as a variable. In the pattern
|
||||
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}. There
|
||||
|
|
@ -74,10 +78,22 @@ class CaptureVariablePathElement extends PathElement {
|
|||
return false;
|
||||
}
|
||||
|
||||
String substringForDecoding = null;
|
||||
CharSequence candidateCapture = null;
|
||||
if (this.constraintPattern != null) {
|
||||
// TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern
|
||||
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
|
||||
if (includesPercent(matchingContext.candidate, candidateIndex, nextPos)) {
|
||||
substringForDecoding = new String(matchingContext.candidate, candidateIndex, nextPos);
|
||||
try {
|
||||
candidateCapture = UriUtils.decode(substringForDecoding,StandardCharsets.UTF_8.name());
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
|
||||
}
|
||||
Matcher matcher = constraintPattern.matcher(candidateCapture);
|
||||
if (matcher.groupCount() != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
|
|
@ -115,7 +131,8 @@ class CaptureVariablePathElement extends PathElement {
|
|||
|
||||
if (match && matchingContext.extractingVariables) {
|
||||
matchingContext.set(this.variableName,
|
||||
new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex));
|
||||
candidateCapture != null ? candidateCapture.toString():
|
||||
decode(new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex)));
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,13 @@
|
|||
|
||||
package org.springframework.web.util.pattern;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PatternParseException.PatternMessage;
|
||||
|
||||
/**
|
||||
|
|
@ -162,7 +165,7 @@ class InternalPathPatternParser {
|
|||
this.variableCaptureCount++;
|
||||
}
|
||||
else if (ch == ':') {
|
||||
if (this.insideVariableCapture) {
|
||||
if (this.insideVariableCapture && !this.isCaptureTheRestVariable) {
|
||||
skipCaptureRegex();
|
||||
this.insideVariableCapture = false;
|
||||
this.variableCaptureCount++;
|
||||
|
|
@ -304,6 +307,26 @@ class InternalPathPatternParser {
|
|||
|
||||
resetPathElementState();
|
||||
}
|
||||
|
||||
private char[] getPathElementText(boolean encodeElement) {
|
||||
char[] pathElementText = new char[this.pos - this.pathElementStart];
|
||||
if (encodeElement) {
|
||||
try {
|
||||
String unencoded = new String(this.pathPatternData, this.pathElementStart, this.pos - this.pathElementStart);
|
||||
String encoded = UriUtils.encodeFragment(unencoded, StandardCharsets.UTF_8.name());
|
||||
pathElementText = encoded.toCharArray();
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// Should never happen...
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0,
|
||||
this.pos - this.pathElementStart);
|
||||
}
|
||||
return pathElementText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used the knowledge built up whilst processing since the last path element to determine what kind of path
|
||||
|
|
@ -314,10 +337,7 @@ class InternalPathPatternParser {
|
|||
if (this.insideVariableCapture) {
|
||||
throw new PatternParseException(this.pos, this.pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
|
||||
char[] pathElementText = new char[this.pos - this.pathElementStart];
|
||||
System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0,
|
||||
this.pos - this.pathElementStart);
|
||||
|
||||
PathElement newPE = null;
|
||||
|
||||
if (this.variableCaptureCount > 0) {
|
||||
|
|
@ -325,12 +345,12 @@ class InternalPathPatternParser {
|
|||
this.pathPatternData[this.pos - 1] == '}') {
|
||||
if (this.isCaptureTheRestVariable) {
|
||||
// It is {*....}
|
||||
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
|
||||
newPE = new CaptureTheRestPathElement(pathElementStart, getPathElementText(false), separator);
|
||||
}
|
||||
else {
|
||||
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
||||
try {
|
||||
newPE = new CaptureVariablePathElement(this.pathElementStart, pathElementText,
|
||||
newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(false),
|
||||
this.caseSensitive, this.separator);
|
||||
}
|
||||
catch (PatternSyntaxException pse) {
|
||||
|
|
@ -347,8 +367,9 @@ class InternalPathPatternParser {
|
|||
throw new PatternParseException(this.pathElementStart, this.pathPatternData,
|
||||
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||
}
|
||||
RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart, pathElementText,
|
||||
this.caseSensitive, this.pathPatternData, this.separator);
|
||||
RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart,
|
||||
getPathElementText(false), this.caseSensitive,
|
||||
this.pathPatternData, this.separator);
|
||||
for (String variableName : newRegexSection.getVariableNames()) {
|
||||
recordCapturedVariable(this.pathElementStart, variableName);
|
||||
}
|
||||
|
|
@ -361,16 +382,16 @@ class InternalPathPatternParser {
|
|||
newPE = new WildcardPathElement(this.pathElementStart, this.separator);
|
||||
}
|
||||
else {
|
||||
newPE = new RegexPathElement(this.pathElementStart, pathElementText,
|
||||
newPE = new RegexPathElement(this.pathElementStart, getPathElementText(false),
|
||||
this.caseSensitive, this.pathPatternData, this.separator);
|
||||
}
|
||||
}
|
||||
else if (this.singleCharWildcardCount != 0) {
|
||||
newPE = new SingleCharWildcardedPathElement(this.pathElementStart, pathElementText,
|
||||
newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(true),
|
||||
this.singleCharWildcardCount, this.caseSensitive, this.separator);
|
||||
}
|
||||
else {
|
||||
newPE = new LiteralPathElement(this.pathElementStart, pathElementText,
|
||||
newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(true),
|
||||
this.caseSensitive, this.separator);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ class LiteralPathElement extends PathElement {
|
|||
if (this.caseSensitive) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (matchingContext.candidate[candidateIndex++] != this.text[i]) {
|
||||
return false;
|
||||
// TODO unfortunate performance hit here on comparison when encoded data is the less likely case
|
||||
if (i < 3 || matchingContext.candidate[candidateIndex-3] != '%' ||
|
||||
Character.toUpperCase(matchingContext.candidate[candidateIndex-1]) != this.text[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
package org.springframework.web.util.pattern;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +69,7 @@ abstract class PathElement {
|
|||
public abstract boolean matches(int candidatePos, MatchingContext matchingContext);
|
||||
|
||||
/**
|
||||
* Return the length of the path element where captures are considered to be one character long.
|
||||
* @return the length of the path element where captures are considered to be one character long.
|
||||
*/
|
||||
public abstract int getNormalizedLength();
|
||||
|
||||
|
|
@ -98,4 +102,50 @@ abstract class PathElement {
|
|||
matchingContext.candidate[nextIndex] == this.separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an input CharSequence if necessary.
|
||||
* @param toDecode the input char sequence that should be decoded if necessary
|
||||
* @returns the decoded result
|
||||
*/
|
||||
protected String decode(CharSequence toDecode) {
|
||||
CharSequence decoded = toDecode;
|
||||
if (includesPercent(toDecode)) {
|
||||
try {
|
||||
decoded = UriUtils.decode(toDecode.toString(), StandardCharsets.UTF_8.name());
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return decoded.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param char sequence of characters
|
||||
* @param from start position (included in check)
|
||||
* @param to end position (excluded from check)
|
||||
* @return true if the chars array includes a '%' character between the specified positions
|
||||
*/
|
||||
protected boolean includesPercent(char[] chars, int from, int to) {
|
||||
for (int i = from; i < to; i++) {
|
||||
if (chars[i] == '%') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param chars string that may include a '%' character indicating it is encoded
|
||||
* @return true if the string contains a '%' character
|
||||
*/
|
||||
protected boolean includesPercent(CharSequence chars) {
|
||||
for (int i = 0, max = chars.length(); i < max; i++) {
|
||||
if (chars.charAt(i) == '%') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
package org.springframework.web.util.pattern;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
|
|
@ -39,7 +42,7 @@ class RegexPathElement extends PathElement {
|
|||
private final String DEFAULT_VARIABLE_PATTERN = "(.*)";
|
||||
|
||||
|
||||
private final char[] regex;
|
||||
private char[] regex;
|
||||
|
||||
private final boolean caseSensitive;
|
||||
|
||||
|
|
@ -61,17 +64,20 @@ class RegexPathElement extends PathElement {
|
|||
public Pattern buildPattern(char[] regex, char[] completePattern) {
|
||||
StringBuilder patternBuilder = new StringBuilder();
|
||||
String text = new String(regex);
|
||||
StringBuilder encodedRegexBuilder = new StringBuilder();
|
||||
Matcher matcher = GLOB_PATTERN.matcher(text);
|
||||
int end = 0;
|
||||
|
||||
while (matcher.find()) {
|
||||
patternBuilder.append(quote(text, end, matcher.start()));
|
||||
patternBuilder.append(quote(text, end, matcher.start(), encodedRegexBuilder));
|
||||
String match = matcher.group();
|
||||
if ("?".equals(match)) {
|
||||
patternBuilder.append('.');
|
||||
encodedRegexBuilder.append('?');
|
||||
}
|
||||
else if ("*".equals(match)) {
|
||||
patternBuilder.append(".*");
|
||||
encodedRegexBuilder.append('*');
|
||||
int pos = matcher.start();
|
||||
if (pos < 1 || text.charAt(pos-1) != '.') {
|
||||
// To be compatible with the AntPathMatcher comparator,
|
||||
|
|
@ -80,6 +86,7 @@ class RegexPathElement extends PathElement {
|
|||
}
|
||||
}
|
||||
else if (match.startsWith("{") && match.endsWith("}")) {
|
||||
encodedRegexBuilder.append(match);
|
||||
int colonIdx = match.indexOf(':');
|
||||
if (colonIdx == -1) {
|
||||
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
|
||||
|
|
@ -106,7 +113,8 @@ class RegexPathElement extends PathElement {
|
|||
end = matcher.end();
|
||||
}
|
||||
|
||||
patternBuilder.append(quote(text, end, text.length()));
|
||||
patternBuilder.append(quote(text, end, text.length(), encodedRegexBuilder));
|
||||
this.regex = encodedRegexBuilder.toString().toCharArray();
|
||||
if (this.caseSensitive) {
|
||||
return Pattern.compile(patternBuilder.toString());
|
||||
}
|
||||
|
|
@ -119,17 +127,33 @@ class RegexPathElement extends PathElement {
|
|||
return this.variableNames;
|
||||
}
|
||||
|
||||
private String quote(String s, int start, int end) {
|
||||
private String quote(String s, int start, int end, StringBuilder encodedRegexBuilder) {
|
||||
if (start == end) {
|
||||
return "";
|
||||
}
|
||||
return Pattern.quote(s.substring(start, end));
|
||||
String substring = s.substring(start, end);
|
||||
try {
|
||||
String encodedSubString = UriUtils.encodePath(substring, StandardCharsets.UTF_8.name());
|
||||
encodedRegexBuilder.append(encodedSubString);
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return Pattern.quote(substring);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
int pos = matchingContext.scanAhead(candidateIndex);
|
||||
Matcher matcher = this.pattern.matcher(new SubSequence(matchingContext.candidate, candidateIndex, pos));
|
||||
|
||||
CharSequence textToMatch = null;
|
||||
if (includesPercent(matchingContext.candidate, candidateIndex, pos)) {
|
||||
textToMatch = decode(new SubSequence(matchingContext.candidate, candidateIndex, pos));
|
||||
}
|
||||
else {
|
||||
textToMatch = new SubSequence(matchingContext.candidate, candidateIndex, pos);
|
||||
}
|
||||
Matcher matcher = this.pattern.matcher(textToMatch);
|
||||
boolean matches = matcher.matches();
|
||||
|
||||
if (matches) {
|
||||
|
|
|
|||
|
|
@ -65,8 +65,18 @@ class SingleCharWildcardedPathElement extends PathElement {
|
|||
if (this.caseSensitive) {
|
||||
for (int i = 0; i <this.len; i++) {
|
||||
char t = this.text[i];
|
||||
if (t != '?' && candidate[candidateIndex] != t) {
|
||||
return false;
|
||||
if (t == '?') {
|
||||
if (candidate[candidateIndex] == '%') {
|
||||
// encoded value, skip next two as well!
|
||||
candidateIndex += 2;
|
||||
}
|
||||
}
|
||||
else if (candidate[candidateIndex] != t) {
|
||||
// TODO unfortunate performance hit here on comparison when encoded data is the less likely case
|
||||
if (i < 3 || matchingContext.candidate[candidateIndex-2] != '%' ||
|
||||
Character.toUpperCase(matchingContext.candidate[candidateIndex]) != this.text[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
candidateIndex++;
|
||||
}
|
||||
|
|
@ -74,7 +84,13 @@ class SingleCharWildcardedPathElement extends PathElement {
|
|||
else {
|
||||
for (int i = 0; i < this.len; i++) {
|
||||
char t = this.text[i];
|
||||
if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) {
|
||||
if (t == '?') {
|
||||
if (candidate[candidateIndex] == '%') {
|
||||
// encoded value, skip next two as well!
|
||||
candidateIndex += 2;
|
||||
}
|
||||
}
|
||||
else if (Character.toLowerCase(candidate[candidateIndex]) != t) {
|
||||
return false;
|
||||
}
|
||||
candidateIndex++;
|
||||
|
|
@ -117,7 +133,7 @@ class SingleCharWildcardedPathElement extends PathElement {
|
|||
|
||||
|
||||
public String toString() {
|
||||
return "SingleCharWildcarding(" + String.valueOf(this.text) + ")";
|
||||
return "SingleCharWildcarded(" + String.valueOf(this.text) + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ public class PathPatternMatcherTests {
|
|||
checkNoMatch("foo", "foobar");
|
||||
checkMatches("/foo/bar", "/foo/bar");
|
||||
checkNoMatch("/foo/bar", "/foo/baz");
|
||||
// TODO Need more tests for escaped separators in path patterns and paths?
|
||||
checkMatches("/foo\\/bar", "/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -254,6 +252,108 @@ public class PathPatternMatcherTests {
|
|||
assertEquals("a/",parse("/").getPathRemaining("/a/").getPathRemaining());
|
||||
assertEquals("/bar",parse("/a{abc}").getPathRemaining("/a/bar").getPathRemaining());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingAndBoundVariablesCapturePathElement() {
|
||||
checkCapture("{var}","f%20o","var","f o");
|
||||
checkCapture("{var1}/{var2}","f%20o/f%7Co","var1","f o","var2","f|o");
|
||||
checkCapture("{var1}/{var2}","f%20o/f%7co","var1","f o","var2","f|o"); // lower case encoding
|
||||
// constraints
|
||||
// - constraint is expressed in non encoded form
|
||||
// - returned values are decoded
|
||||
checkCapture("{var:foo}","foo","var","foo");
|
||||
checkCapture("{var:f o}","f%20o","var","f o"); // constraint is expressed in non encoded form
|
||||
checkCapture("{var:f.o}","f%20o","var","f o");
|
||||
checkCapture("{var:f\\|o}","f%7co","var","f|o");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingAndBoundVariablesCaptureTheRestPathElement() {
|
||||
checkCapture("/{*var}","/f%20o","var","/f o");
|
||||
checkCapture("{var1}/{*var2}","f%20o/f%7Co","var1","f o","var2","/f|o");
|
||||
// constraints - decoding happens for constraint checking but returned value is undecoded
|
||||
checkCapture("/{*var}","/foo","var","/foo");
|
||||
checkCapture("/{*var}","/f%20o","var","/f o"); // constraint is expressed in non encoded form
|
||||
checkCapture("/{*var}","/f%20o","var","/f o");
|
||||
checkCapture("/{*var}","/f%7co","var","/f|o");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingWithCaseSensitivity() {
|
||||
// Concern here is that regardless of case sensitivity, %7c == %7C (for example)
|
||||
// Need to test all path elements that might have literal components
|
||||
|
||||
PathPatternParser ppp = new PathPatternParser();
|
||||
ppp.setCaseSensitive(true);
|
||||
|
||||
// LiteralPathElement
|
||||
PathPattern pp = ppp.parse("/this is a |");
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7c"));
|
||||
assertFalse(pp.matches("/thIs%20is%20a%20%7c"));
|
||||
assertFalse(pp.matches("/thIs%20is%20a%20%7C"));
|
||||
assertEquals("Separator(/) Literal(this%20is%20a%20%7C)",pp.toChainString());
|
||||
|
||||
// RegexPathElement
|
||||
pp = ppp.parse("/{foo}this is a |");
|
||||
assertTrue(pp.matches("/xxxthis%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/xxxthis%20is%20a%20%7c"));
|
||||
assertFalse(pp.matches("/xxxXhis%20is%20a%20%7C"));
|
||||
assertFalse(pp.matches("/xxxXhis%20is%20a%20%7c"));
|
||||
assertEquals("Separator(/) Regex({foo}this%20is%20a%20%7C)",pp.toChainString());
|
||||
|
||||
// SingleCharWildcardedPathElement
|
||||
pp = ppp.parse("/th?s is a |");
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7c"));
|
||||
assertFalse(pp.matches("/xhis%20is%20a%20%7C"));
|
||||
assertFalse(pp.matches("/xhis%20is%20a%20%7c"));
|
||||
assertEquals("Separator(/) SingleCharWildcarded(th?s%20is%20a%20%7C)",pp.toChainString());
|
||||
|
||||
ppp = new PathPatternParser();
|
||||
ppp.setCaseSensitive(false);
|
||||
|
||||
// LiteralPathElement
|
||||
pp = ppp.parse("/this is a |");
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7c"));
|
||||
assertTrue(pp.matches("/thIs%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/tHis%20is%20a%20%7c"));
|
||||
// For case insensitive matches we make all the chars lower case
|
||||
assertEquals("Separator(/) Literal(this%20is%20a%20%7c)",pp.toChainString());
|
||||
|
||||
// RegexPathElement
|
||||
pp = ppp.parse("/{foo}this is a |");
|
||||
assertTrue(pp.matches("/xxxthis%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/xxxthis%20is%20a%20%7c"));
|
||||
assertTrue(pp.matches("/xxxThis%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/xxxThis%20is%20a%20%7c"));
|
||||
assertEquals("Separator(/) Regex({foo}this%20is%20a%20%7C)",pp.toChainString());
|
||||
|
||||
// SingleCharWildcardedPathElement
|
||||
pp = ppp.parse("/th?s is a |");
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/this%20is%20a%20%7c"));
|
||||
assertTrue(pp.matches("/This%20is%20a%20%7C"));
|
||||
assertTrue(pp.matches("/This%20is%20a%20%7c"));
|
||||
assertEquals("Separator(/) SingleCharWildcarded(th?s%20is%20a%20%7c)",pp.toChainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingAndBoundVariablesRegexPathElement() {
|
||||
checkCapture("/{var1:f o}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
|
||||
checkCapture("/{var1}_{var2}","/f%20o_foo","var1","f o","var2","foo");
|
||||
checkCapture("/{var1}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
|
||||
checkCapture("/{var1}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
|
||||
checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodedPaths() {
|
||||
checkMatches("/foo bar", "/foo%20bar");
|
||||
checkMatches("/foo*bar", "/fooboobar");
|
||||
checkMatches("/f?o","/f%7co");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathRemainingCornerCases_spr15336() {
|
||||
|
|
@ -303,6 +403,7 @@ public class PathPatternMatcherTests {
|
|||
checkNoMatch("tes?", "tsst");
|
||||
checkMatches(".?.a", ".a.a");
|
||||
checkNoMatch(".?.a", ".aba");
|
||||
checkMatches("/f?o/bar","/f%20o/bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -22,20 +22,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.web.util.pattern.CaptureTheRestPathElement;
|
||||
import org.springframework.web.util.pattern.CaptureVariablePathElement;
|
||||
import org.springframework.web.util.pattern.LiteralPathElement;
|
||||
import org.springframework.web.util.pattern.PathElement;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import org.springframework.web.util.pattern.PatternParseException;
|
||||
import org.springframework.web.util.pattern.PatternParseException.PatternMessage;
|
||||
import org.springframework.web.util.pattern.RegexPathElement;
|
||||
import org.springframework.web.util.pattern.SeparatorPathElement;
|
||||
import org.springframework.web.util.pattern.SingleCharWildcardedPathElement;
|
||||
import org.springframework.web.util.pattern.WildcardPathElement;
|
||||
import org.springframework.web.util.pattern.WildcardTheRestPathElement;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
|
@ -82,7 +69,7 @@ public class PathPatternParserTests {
|
|||
assertEquals("Literal(abc)", checkStructure("abc").toChainString());
|
||||
assertEquals("Regex({a}_*_{b})", checkStructure("{a}_*_{b}").toChainString());
|
||||
assertEquals("Separator(/)", checkStructure("/").toChainString());
|
||||
assertEquals("SingleCharWildcarding(?a?b?c)", checkStructure("?a?b?c").toChainString());
|
||||
assertEquals("SingleCharWildcarded(?a?b?c)", checkStructure("?a?b?c").toChainString());
|
||||
assertEquals("Wildcard(*)", checkStructure("*").toChainString());
|
||||
assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString());
|
||||
}
|
||||
|
|
@ -100,6 +87,7 @@ public class PathPatternParserTests {
|
|||
checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
checkError("/{f*oobar}", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
|
||||
checkError("/{*foobar}/abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
checkError("/{*foobar:.*}/abc", 9, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
|
||||
checkError("/{abc}{*foobar}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||
checkError("/{abc}{*foobar}{foo}", 15, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
}
|
||||
|
|
@ -193,6 +181,73 @@ public class PathPatternParserTests {
|
|||
checkStructure("/{f}/");
|
||||
checkStructure("/{foo}/{bar}/{wibble}");
|
||||
}
|
||||
|
||||
/**
|
||||
* During a parse some elements of the path are encoded for use when matching an encoded path.
|
||||
* The patterns a developer writes are not encoded, hence we decode them when turning them
|
||||
* into PathPattern objects. The encoding is visible through the toChainString() method.
|
||||
*/
|
||||
@Test
|
||||
public void encodingDuringParse() throws Exception {
|
||||
PathPattern pp;
|
||||
|
||||
// CaptureTheRest
|
||||
pp = parse("/{*var}");
|
||||
assertEquals("CaptureTheRest(/{*var})",pp.toChainString());
|
||||
|
||||
// CaptureVariable
|
||||
pp = parse("/{var}");
|
||||
assertEquals("Separator(/) CaptureVariable({var})",pp.toChainString());
|
||||
|
||||
// Literal
|
||||
pp = parse("/foo bar/b_oo");
|
||||
assertEquals("Separator(/) Literal(foo%20bar) Separator(/) Literal(b_oo)",pp.toChainString());
|
||||
pp = parse("foo:bar");
|
||||
assertEquals("Literal(foo:bar)",pp.toChainString());
|
||||
|
||||
// Regex
|
||||
pp = parse("{foo}_{bar}");
|
||||
assertEquals("Regex({foo}_{bar})",pp.toChainString());
|
||||
pp = parse("{foo}_ _{bar}");
|
||||
assertEquals("Regex({foo}_%20_{bar})",pp.toChainString());
|
||||
|
||||
// Separator
|
||||
pp = parse("/");
|
||||
assertEquals("Separator(/)",pp.toChainString());
|
||||
|
||||
// SingleCharWildcarded
|
||||
pp = parse("/foo?bar");
|
||||
assertEquals("Separator(/) SingleCharWildcarded(foo?bar)",pp.toChainString());
|
||||
pp = parse("/f o?bar");
|
||||
assertEquals("Separator(/) SingleCharWildcarded(f%20o?bar)",pp.toChainString());
|
||||
|
||||
// Wildcard
|
||||
pp = parse("/foo*bar");
|
||||
assertEquals("Separator(/) Regex(foo*bar)",pp.toChainString());
|
||||
pp = parse("f oo:*bar");
|
||||
assertEquals("Regex(f%20oo:*bar)",pp.toChainString());
|
||||
pp = parse("/f oo:*bar");
|
||||
assertEquals("Separator(/) Regex(f%20oo:*bar)",pp.toChainString());
|
||||
pp = parse("/f|!oo:*bar");
|
||||
assertEquals("Separator(/) Regex(f%7C!oo:*bar)",pp.toChainString());
|
||||
|
||||
// WildcardTheRest
|
||||
pp = parse("/**");
|
||||
assertEquals("WildcardTheRest(/**)",pp.toChainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingWithConstraints() {
|
||||
// Constraint regex expressions are not URL encoded
|
||||
PathPattern pp = parse("/{var:f o}");
|
||||
assertEquals("Separator(/) CaptureVariable({var:f o})",pp.toChainString());
|
||||
|
||||
pp = parse("/{var:f o}_");
|
||||
assertEquals("Separator(/) Regex({var:f o}_)",pp.toChainString());
|
||||
|
||||
pp = parse("{foo:f o}_ _{bar:b\\|o}");
|
||||
assertEquals("Regex({foo:f o}_%20_{bar:b\\|o})",pp.toChainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeCaptureWithConstraints() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue