Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.net.PercentEscaper;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
Expand Down Expand Up @@ -881,19 +881,22 @@ private String instantiate(Map<String, String> values, boolean allowPartial) {
continueLast = true;
continue;
}
boolean hasPathWildcard = hasPathWildcard(iterator);
Segment next = iterator.next();
Segment nextNext = iterator.next();
boolean pathEscape =
next.kind() == SegmentKind.PATH_WILDCARD
|| nextNext.kind() != SegmentKind.END_BINDING;
restore(iterator, iterator.nextIndex() - 2);
if (!pathEscape) {
validatePathSegment(var, value, value, hasPathWildcard);
result.append(encodeUrl(value));
} else {
// For a path wildcard or path of length greater 1, split the value and escape
// every sub-segment.
boolean first = true;
for (String subSeg : SLASH_SPLITTER.split(value)) {
validatePathSegment(var, value, subSeg, hasPathWildcard);
if (!first) {
result.append('/');
}
Expand Down Expand Up @@ -1208,13 +1211,38 @@ private static List<Segment> parseComplexResourceId(String seg) {
// Helpers
// =======

private static final PercentEscaper URL_ESCAPER = new PercentEscaper("-_.~", false);

private static boolean hasPathWildcard(ListIterator<Segment> iterator) {
int start = iterator.nextIndex();
boolean found = false;
while (iterator.hasNext()) {
Segment seg = iterator.next();
if (seg.kind() == SegmentKind.END_BINDING) {
break;
}
if (seg.kind() == SegmentKind.PATH_WILDCARD) {
found = true;
}
}
restore(iterator, start);
return found;
}
Comment on lines +1216 to +1230

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Once a PATH_WILDCARD segment is found, we can break out of the loop early. Since restore(iterator, start) is called at the end of the method, the iterator's position will be correctly restored regardless of how far we advanced it. This avoids unnecessary iterations over the remaining segments of the binding.

Suggested change
private static boolean hasPathWildcard(ListIterator<Segment> iterator) {
int start = iterator.nextIndex();
boolean found = false;
while (iterator.hasNext()) {
Segment seg = iterator.next();
if (seg.kind() == SegmentKind.END_BINDING) {
break;
}
if (seg.kind() == SegmentKind.PATH_WILDCARD) {
found = true;
}
}
restore(iterator, start);
return found;
}
private static boolean hasPathWildcard(ListIterator<Segment> iterator) {
int start = iterator.nextIndex();
boolean found = false;
while (iterator.hasNext()) {
Segment seg = iterator.next();
if (seg.kind() == SegmentKind.END_BINDING) {
break;
}
if (seg.kind() == SegmentKind.PATH_WILDCARD) {
found = true;
break;
}
}
restore(iterator, start);
return found;
}


private static void validatePathSegment(
String var, String fullValue, String segment, boolean hasPathWildcard) {
if (".".equals(segment) || "..".equals(segment)) {
if (hasPathWildcard) {
throw new ValidationException(
"Value for %s must not contain segments that are exactly . or ..", var);
}
throw new ValidationException("Invalid value %s for %s.", fullValue, var);
}
}

private String encodeUrl(String text) {
if (urlEncoding) {
try {
return URLEncoder.encode(text, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new ValidationException("UTF-8 encoding is not supported on this platform");
}
return URL_ESCAPER.escape(text);
} else {
// When encoding is disabled, we accept any character except '/'
final String INVALID_CHAR = "/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,90 @@ void instantiateEscapeUnsafeChar() {
.isEqualTo("buckets/f%2Fo%2Fo/objects/b%2Fa%2Fr");
}

@Test
void instantiateRejectsDotAndDoubleDotInSingleWildcard() {
PathTemplate template =
PathTemplate.create("/compute/v1/projects/{project}/regions/{region}/addresses");
ValidationException dotDotException =
Assertions.assertThrows(
ValidationException.class,
() -> template.instantiate("project", "my-project", "region", ".."));
Truth.assertThat(dotDotException.getMessage()).isEqualTo("Invalid value .. for region.");

ValidationException dotException =
Assertions.assertThrows(
ValidationException.class,
() -> template.instantiate("project", "my-project", "region", "."));
Truth.assertThat(dotException.getMessage()).isEqualTo("Invalid value . for region.");
}

@Test
void instantiateRejectsDotAndDoubleDotInMultiSegmentWildcard() {
PathTemplate template =
PathTemplate.create("/v2/{parent=projects/*/locations/*}/content:inspect");
ValidationException dotException =
Assertions.assertThrows(
ValidationException.class,
() -> template.instantiate("parent", "projects/my-project/locations/."));
Truth.assertThat(dotException.getMessage())
.isEqualTo("Invalid value projects/my-project/locations/. for parent.");

ValidationException dotDotException =
Assertions.assertThrows(
ValidationException.class,
() -> template.instantiate("parent", "projects/my-project/locations/.."));
Truth.assertThat(dotDotException.getMessage())
.isEqualTo("Invalid value projects/my-project/locations/.. for parent.");
}

@Test
void instantiateRejectsDotAndDoubleDotInPathWildcard() {
PathTemplate template =
PathTemplate.create("/v3/{name=projects/*/monitoredResourceDescriptors/**}");
for (String invalidName :
new String[] {
"projects/my-project/monitoredResourceDescriptors/instance/my-instance/..",
"projects/my-project/monitoredResourceDescriptors/instance/my-instance/.",
"projects/my-project/monitoredResourceDescriptors/.",
"projects/my-project/monitoredResourceDescriptors/..",
"projects/my-project/monitoredResourceDescriptors/instance/./my-instance/..",
"projects/my-project/monitoredResourceDescriptors/instance/../.."
}) {
ValidationException exception =
Assertions.assertThrows(
ValidationException.class, () -> template.instantiate("name", invalidName));
Truth.assertThat(exception.getMessage())
.isEqualTo("Value for name must not contain segments that are exactly . or ..");
}
}

@Test
void instantiateAllowsSafeDotsAndPercentEncodesReservedChars() {
PathTemplate template = PathTemplate.create("/v1/{x=projects/*/databases/*/documents/**}");
Truth.assertThat(
template.instantiate("x", "projects/sys-prod-123/databases/default/documents/my.txt"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/my.txt");
Truth.assertThat(
template.instantiate(
"x", "projects/sys-prod-123/databases/default/documents/my..file.txt"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/my..file.txt");
Truth.assertThat(
template.instantiate(
"x", "projects/sys-prod-123/databases/default/documents/doc?key=val"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%3Fkey%3Dval");
Truth.assertThat(
template.instantiate("x", "projects/sys-prod-123/databases/default/documents/doc#frag"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%23frag");
Truth.assertThat(
template.instantiate(
"x", "projects/sys-prod-123/databases/default/documents/doc with space"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%20with%20space");
Truth.assertThat(
template.instantiate(
"x", "projects/sys-prod-123/databases/default/documents/%2e%2e/escape-db"))
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/%252e%252e/escape-db");
}

@Test
void instantiateNotEscapeForUnboundedWildcard() {
PathTemplate template = PathTemplate.create("buckets/*/objects/**");
Expand Down
Loading