Update variable detection in UriComponentsBuilder#encode

This commit better aligns how URI variable placeholders are detected
in UriComponentsBuilder#encode (i.e. the pre-encoding of the literal
parts of a URI template) and how they are expanded later on.
The latter relies on a pattern that stops at the first closing '}'
which excludes the possibility for well-formed, nested placeholders
other than variables with regex syntax, e.g. "{year:\d{1,4}}".

UriComponentsBuilder#encode now also stops at the first closing '}' and
further ensures the placeholder is not empty and that it has '{' before
deciding to treat it as a URI variable.

Closes gh-26466
This commit is contained in:
Rossen Stoyanchev
2021-02-10 21:10:03 +00:00
parent c9147c4281
commit 8791928f61
5 changed files with 90 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -767,44 +767,47 @@ final class HierarchicalUriComponents extends UriComponents {
private final StringBuilder output = new StringBuilder();
private boolean variableWithNameAndRegex;
public UriTemplateEncoder(Charset charset) {
this.charset = charset;
}
@Override
public String apply(String source, Type type) {
// Only URI variable (nothing to encode)..
if (source.length() > 1 && source.charAt(0) == '{' && source.charAt(source.length() -1) == '}') {
// URI variable only?
if (isUriVariable(source)) {
return source;
}
// Only literal (encode full source)..
// Literal template only?
if (source.indexOf('{') == -1) {
return encodeUriComponent(source, this.charset, type);
}
// Mixed literal parts and URI variables, maybe (encode literal parts only)..
int level = 0;
clear(this.currentLiteral);
clear(this.currentVariable);
clear(this.output);
for (int i = 0; i < source.length(); i++) {
char c = source.charAt(i);
if (c == ':' && level == 1) {
this.variableWithNameAndRegex = true;
}
if (c == '{') {
level++;
if (level == 1) {
encodeAndAppendCurrentLiteral(type);
append(this.currentLiteral, true, type);
}
}
if (c == '}' && level > 0) {
level--;
this.currentVariable.append('}');
if (level == 0) {
this.output.append(this.currentVariable);
clear(this.currentVariable);
boolean encode = !isUriVariable(this.currentVariable);
append(this.currentVariable, encode, type);
}
else if (!this.variableWithNameAndRegex) {
append(this.currentVariable, true, type);
level = 0;
}
}
else if (level > 0) {
@@ -817,13 +820,38 @@ final class HierarchicalUriComponents extends UriComponents {
if (level > 0) {
this.currentLiteral.append(this.currentVariable);
}
encodeAndAppendCurrentLiteral(type);
append(this.currentLiteral, true, type);
return this.output.toString();
}
private void encodeAndAppendCurrentLiteral(Type type) {
this.output.append(encodeUriComponent(this.currentLiteral.toString(), this.charset, type));
clear(this.currentLiteral);
/**
* Whether the given String is a single URI variable that can be
* expanded. It must have '{' and '}' surrounding non-empty text and no
* nested placeholders unless it is a variable with regex syntax,
* e.g. {@code "/{year:\d{1,4}}"}.
*/
private boolean isUriVariable(CharSequence source) {
if (source.length() < 2 || source.charAt(0) != '{' || source.charAt(source.length() -1) != '}') {
return false;
}
boolean hasText = false;
for (int i = 1; i < source.length() - 1; i++) {
char c = source.charAt(i);
if (c == ':' && i > 1) {
return true;
}
if (c == '{' || c == '}') {
return false;
}
hasText = (hasText || !Character.isWhitespace(c));
}
return hasText;
}
private void append(StringBuilder sb, boolean encode, Type type) {
this.output.append(encode ? encodeUriComponent(sb.toString(), this.charset, type) : sb);
clear(sb);
this.variableWithNameAndRegex = false;
}
private void clear(StringBuilder sb) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -201,10 +201,11 @@ public abstract class UriComponents implements Serializable {
/**
* Concatenate all URI components to return the fully formed URI String.
* <p>This method does nothing more than a simple concatenation based on
* current values. That means it could produce different results if invoked
* before vs after methods that can change individual values such as
* {@code encode}, {@code expand}, or {@code normalize}.
* <p>This method amounts to simple String concatenation of the current
* URI component values and as such the result may contain illegal URI
* characters, for example if URI variables have not been expanded or if
* encoding has not been applied via {@link UriComponentsBuilder#encode()}
* or {@link #encode()}.
*/
public abstract String toUriString();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -412,12 +412,15 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
* also escaping characters with reserved meaning.
* <p>For most cases, this method is more likely to give the expected result
* because in treats URI variables as opaque data to be fully encoded, while
* {@link UriComponents#encode()} is useful only if intentionally expanding
* URI variables that contain reserved characters.
* {@link UriComponents#encode()} is useful when intentionally expanding URI
* variables that contain reserved characters.
* <p>For example ';' is legal in a path but has reserved meaning. This
* method replaces ";" with "%3B" in URI variables but not in the URI
* template. By contrast, {@link UriComponents#encode()} never replaces ";"
* since it is a legal character in a path.
* <p>When not expanding URI variables at all, prefer use of
* {@link UriComponents#encode()} since that will also encode anything that
* incidentally looks like a URI variable.
* @since 5.0.8
*/
public final UriComponentsBuilder encode() {