Skip to content

Commit 30766a8

Browse files
fix: UriTemplate expansion reserved ("+") and fragment("#") should not encode already percent encoded parts (#2108)
* Added new escaper for already percent encoded inputs * Added the new escaper to the CharEscapers utility class * Fixed the inconsistency with rfc6570#section-3.2.1 * Fix linter error * Update documentation to clarify support for URI Template levels * Change PercentEncodedEscaper class to package-private --------- Co-authored-by: Diego Marquez <[email protected]>
1 parent 5d0689a commit 30766a8

File tree

5 files changed

+157
-3
lines changed

5 files changed

+157
-3
lines changed

google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
/**
3030
* Expands URI Templates.
3131
*
32-
* <p>This Class supports Level 1 templates and all Level 4 composite templates as described in: <a
33-
* href="http://tools.ietf.org/html/rfc6570">RFC 6570</a>.
32+
* <p>This class supports URI Template Level 1, partial support for Levels 2 and 3, and Level 4
33+
* composite templates as described in: <a href="http://tools.ietf.org/html/rfc6570">RFC 6570</a>.
3434
*
3535
* <p>Specifically, for the variables: var := "value" list := ["red", "green", "blue"] keys :=
3636
* [("semi", ";"),("dot", "."),("comma", ",")]
@@ -159,7 +159,7 @@ private String getEncodedValue(String value) {
159159
String encodedValue;
160160
if (reservedExpansion) {
161161
// Reserved expansion allows percent-encoded triplets and characters in the reserved set.
162-
encodedValue = CharEscapers.escapeUriPathWithoutReserved(value);
162+
encodedValue = CharEscapers.escapeUriPathWithoutReservedAndPercentEncoded(value);
163163
} else {
164164
encodedValue = CharEscapers.escapeUriConformant(value);
165165
}

google-http-client/src/main/java/com/google/api/client/util/escape/CharEscapers.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public final class CharEscapers {
4444
private static final Escaper URI_QUERY_STRING_ESCAPER =
4545
new PercentEscaper(PercentEscaper.SAFEQUERYSTRINGCHARS_URLENCODER);
4646

47+
private static final Escaper URI_RESERVED_AND_PERCENT_ENCODED_ESCAPER =
48+
new PercentEncodedEscaper(URI_RESERVED_ESCAPER);
49+
4750
/**
4851
* Escapes the string value so it can be safely included in application/x-www-form-urlencoded
4952
* data. This is not appropriate for generic URI escaping. In particular it encodes the space
@@ -184,6 +187,15 @@ public static String escapeUriPathWithoutReserved(String value) {
184187
return URI_RESERVED_ESCAPER.escape(value);
185188
}
186189

190+
/**
191+
* Escapes a URI path but retains all reserved and percent-encoded characters. That is the same as
192+
* {@link #escapeUriPathWithoutReserved(String)} except that it also escapes percent encoded
193+
* parts.
194+
*/
195+
public static String escapeUriPathWithoutReservedAndPercentEncoded(String value) {
196+
return URI_RESERVED_AND_PERCENT_ENCODED_ESCAPER.escape(value);
197+
}
198+
187199
/**
188200
* Escapes the string value so it can be safely included in URI user info part. For details on
189201
* escaping URIs, see <a href="http://tools.ietf.org/html/rfc3986#section-2.4">RFC 3986 - section
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.google.api.client.util.escape;
2+
3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
6+
/**
7+
* An {@link Escaper} implementation that preserves percent-encoded sequences in the input string.
8+
*
9+
* <p>This escaper applies the provided {@link Escaper} to all parts of the input string except for
10+
* valid percent-encoded sequences (e.g., <code>%20</code>), which are left unchanged.
11+
*/
12+
final class PercentEncodedEscaper extends Escaper {
13+
14+
/** Pattern to match valid percent-encoded sequences (e.g., %20). */
15+
static final Pattern PCT_ENCODE_PATTERN = Pattern.compile("%[0-9A-Fa-f]{2}");
16+
17+
private final Escaper escaper;
18+
19+
public PercentEncodedEscaper(Escaper escaper) {
20+
if (escaper == null) {
21+
throw new NullPointerException("Escaper cannot be null");
22+
}
23+
this.escaper = escaper;
24+
}
25+
26+
/**
27+
* Escapes the input string using the provided {@link Escaper}, preserving valid percent-encoded
28+
* sequences.
29+
*
30+
* @param string the input string to escape
31+
* @return the escaped string with percent-encoded sequences left unchanged
32+
*/
33+
@Override
34+
public String escape(String string) {
35+
if (string == null || string.isEmpty()) {
36+
return string;
37+
}
38+
39+
Matcher matcher = PCT_ENCODE_PATTERN.matcher(string);
40+
StringBuilder sb = new StringBuilder();
41+
42+
int lastEnd = 0;
43+
while (matcher.find()) {
44+
sb.append(escaper.escape(string.substring(lastEnd, matcher.start())));
45+
46+
sb.append(string.substring(matcher.start(), matcher.end()));
47+
48+
lastEnd = matcher.end();
49+
}
50+
51+
if (lastEnd < string.length()) {
52+
sb.append(escaper.escape(string.substring(lastEnd)));
53+
}
54+
55+
return sb.toString();
56+
}
57+
}

google-http-client/src/test/java/com/google/api/client/http/UriTemplateTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,4 +380,57 @@ public void testExpandTemplates_reservedExpansion_mustNotEscapeUnreservedCharSet
380380
unReservedSet,
381381
UriTemplate.expand("{+var}", requestMap, false));
382382
}
383+
384+
@Test
385+
// These tests are from the uri-template test suite
386+
// https://github.com/uri-templates/uritemplate-test/blob/master/extended-tests.json
387+
public void testExpandTemplates_reservedExpansion_alreadyEncodedInput() {
388+
Map<String, Object> variables = Maps.newLinkedHashMap();
389+
variables.put("id", "admin%2F");
390+
assertEquals("admin%252F", UriTemplate.expand("{id}", variables, false));
391+
assertEquals("admin%2F", UriTemplate.expand("{+id}", variables, false));
392+
assertEquals("#admin%2F", UriTemplate.expand("{#id}", variables, false));
393+
}
394+
395+
@Test
396+
// These tests are from the uri-template test suite
397+
// https://github.com/uri-templates/uritemplate-test/blob/master/extended-tests.json
398+
public void testExpandTemplates_reservedExpansion_notEncodedInput() {
399+
Map<String, Object> variables = Maps.newLinkedHashMap();
400+
variables.put("not_pct", "%foo");
401+
assertEquals("%25foo", UriTemplate.expand("{not_pct}", variables, false));
402+
assertEquals("%25foo", UriTemplate.expand("{+not_pct}", variables, false));
403+
assertEquals("#%25foo", UriTemplate.expand("{#not_pct}", variables, false));
404+
}
405+
406+
@Test
407+
// These tests are from the uri-template test suite
408+
// https://github.com/uri-templates/uritemplate-test/blob/master/extended-tests.json
409+
public void testExpandTemplates_reservedExpansion_listExpansionWithMixedEncodedInput() {
410+
Map<String, Object> variables = Maps.newLinkedHashMap();
411+
variables.put("list", Arrays.asList("red%25", "%2Fgreen", "blue "));
412+
assertEquals("red%2525,%252Fgreen,blue%20", UriTemplate.expand("{list}", variables, false));
413+
assertEquals("red%25,%2Fgreen,blue%20", UriTemplate.expand("{+list}", variables, false));
414+
assertEquals("#red%25,%2Fgreen,blue%20", UriTemplate.expand("{#list}", variables, false));
415+
}
416+
417+
@Test
418+
// These tests are from the uri-template test suite
419+
// https://github.com/uri-templates/uritemplate-test/blob/master/extended-tests.json with an
420+
// additional map entry
421+
public void testExpandTemplates_reservedExpansion_mapWithMixedEncodedInput() {
422+
Map<String, Object> variables = Maps.newLinkedHashMap();
423+
Map<String, String> keys = Maps.newLinkedHashMap();
424+
keys.put("key1", "val1%2F");
425+
keys.put("key2", "val2%2F");
426+
keys.put("key3", "val ");
427+
variables.put("keys", keys);
428+
assertEquals(
429+
"key1,val1%252F,key2,val2%252F,key3,val%20",
430+
UriTemplate.expand("{keys}", variables, false));
431+
assertEquals(
432+
"key1,val1%2F,key2,val2%2F,key3,val%20", UriTemplate.expand("{+keys}", variables, false));
433+
assertEquals(
434+
"#key1,val1%2F,key2,val2%2F,key3,val%20", UriTemplate.expand("{#keys}", variables, false));
435+
}
383436
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.google.api.client.util.escape;
2+
3+
import junit.framework.TestCase;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.junit.runners.JUnit4;
7+
8+
@RunWith(JUnit4.class)
9+
public class PercentEncodedEscaperTest extends TestCase {
10+
@Test
11+
public void testEscape() {
12+
PercentEncodedEscaper escaper =
13+
new PercentEncodedEscaper(
14+
new PercentEscaper(PercentEscaper.SAFE_PLUS_RESERVED_CHARS_URLENCODER));
15+
String input = "Hello%20World+/?#[]";
16+
17+
String actual = escaper.escape(input);
18+
assertEquals(input, actual); // No change expected since it's already percent-encoded
19+
}
20+
21+
@Test
22+
public void testEscapeEncode() {
23+
PercentEncodedEscaper escaper =
24+
new PercentEncodedEscaper(
25+
new PercentEscaper(PercentEscaper.SAFE_PLUS_RESERVED_CHARS_URLENCODER));
26+
String input = "Hello World%";
27+
String expected = "Hello%20World%25";
28+
29+
String actual = escaper.escape(input);
30+
assertEquals(expected, actual);
31+
}
32+
}

0 commit comments

Comments
 (0)