Skip to content

Commit

Permalink
Introduce new URL parser
Browse files Browse the repository at this point in the history
This commit introduces a new URL parser based on algorithm provided in
the Living URL standard. This new UrlParser is used by
UriComponentsBuilder::fromUriString, replacing the regular expressions.

Closes gh-32513
  • Loading branch information
poutsma committed Apr 17, 2024
1 parent 8727d72 commit f21e05a
Show file tree
Hide file tree
Showing 8 changed files with 2,530 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,12 @@ public boolean isAllowed(int c) {
public boolean isAllowed(int c) {
return isUnreserved(c);
}
},
C0 {
@Override
public boolean isAllowed(int c) {
return !(c >= 0 && c <= 0x1f) && !(c > '~');
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.util;

/**
* Thrown when a URL string cannot be parsed.
*
* @author Arjen Poutsma
* @since 6.2
*/
public class InvalidUrlException extends IllegalArgumentException {

private static final long serialVersionUID = 7409308391039105562L;


/**
* Construct a {@code InvalidUrlException} with the specified detail message.
* @param msg the detail message
*/
public InvalidUrlException(String msg) {
super(msg);
}

/**
* Construct a {@code InvalidUrlException} with the specified detail message
* and nested exception.
* @param msg the detail message
* @param cause the nested exception
*/
public InvalidUrlException(String msg, Throwable cause) {
super(msg, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,10 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
"^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");

private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");

private static final Object[] EMPTY_VALUES = new Object[0];

private static final UrlParser.UrlRecord EMPTY_URL_RECORD = new UrlParser.UrlRecord();


@Nullable
private String scheme;
Expand Down Expand Up @@ -214,52 +212,45 @@ public static UriComponentsBuilder fromUri(URI uri) {
* </pre>
* @param uri the URI string to initialize with
* @return the new {@code UriComponentsBuilder}
* @throws InvalidUrlException if {@code uri} cannot be parsed
*/
public static UriComponentsBuilder fromUriString(String uri) {
public static UriComponentsBuilder fromUriString(String uri) throws InvalidUrlException {
Assert.notNull(uri, "URI must not be null");
Matcher matcher = URI_PATTERN.matcher(uri);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
String scheme = matcher.group(2);
String userInfo = matcher.group(5);
String host = matcher.group(6);
String port = matcher.group(8);
String path = matcher.group(9);
String query = matcher.group(11);
String fragment = matcher.group(13);
boolean opaque = false;
if (StringUtils.hasLength(scheme)) {
String rest = uri.substring(scheme.length());
if (!rest.startsWith(":/")) {
opaque = true;
}

UriComponentsBuilder builder = new UriComponentsBuilder();
if (!uri.isEmpty()) {
UrlParser.UrlRecord urlRecord = UrlParser.parse(uri, EMPTY_URL_RECORD, null, null);
if (!urlRecord.scheme().isEmpty()) {
builder.scheme(urlRecord.scheme());
}
builder.scheme(scheme);
if (opaque) {
String ssp = uri.substring(scheme.length() + 1);
if (StringUtils.hasLength(fragment)) {
ssp = ssp.substring(0, ssp.length() - (fragment.length() + 1));
if (urlRecord.includesCredentials()) {
StringBuilder userInfo = new StringBuilder(urlRecord.username());
if (!urlRecord.password().isEmpty()) {
userInfo.append(':');
userInfo.append(urlRecord.password());
}
builder.schemeSpecificPart(ssp);
builder.userInfo(userInfo.toString());
}
if (urlRecord.host() != null && !(urlRecord.host() instanceof UrlParser.EmptyHost)) {
builder.host(urlRecord.host().toString());
}
if (urlRecord.port() != null) {
builder.port(urlRecord.port());
}
if (urlRecord.path().isOpaque()) {
builder.schemeSpecificPart(urlRecord.path().toString());
}
else {
checkSchemeAndHost(uri, scheme, host);
builder.userInfo(userInfo);
builder.host(host);
if (StringUtils.hasLength(port)) {
builder.port(port);
builder.path(urlRecord.path().toString());
if (StringUtils.hasLength(urlRecord.query())) {
builder.query(urlRecord.query());
}
builder.path(path);
builder.query(query);
}
if (StringUtils.hasText(fragment)) {
builder.fragment(fragment);
if (StringUtils.hasLength(urlRecord.fragment())) {
builder.fragment(urlRecord.fragment());
}
return builder;
}
else {
throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
}
return builder;
}

/**
Expand All @@ -275,33 +266,11 @@ public static UriComponentsBuilder fromUriString(String uri) {
* </pre>
* @param httpUrl the source URI
* @return the URI components of the URI
* @deprecated as of 6.2, in favor of {@link #fromUriString(String)}
*/
public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
Assert.notNull(httpUrl, "HTTP URL must not be null");
Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
if (matcher.matches()) {
UriComponentsBuilder builder = new UriComponentsBuilder();
String scheme = matcher.group(1);
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
builder.userInfo(matcher.group(4));
String host = matcher.group(5);
checkSchemeAndHost(httpUrl, scheme, host);
builder.host(host);
String port = matcher.group(7);
if (StringUtils.hasLength(port)) {
builder.port(port);
}
builder.path(matcher.group(8));
builder.query(matcher.group(10));
String fragment = matcher.group(12);
if (StringUtils.hasText(fragment)) {
builder.fragment(fragment);
}
return builder;
}
else {
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
}
@Deprecated(since = "6.2")
public static UriComponentsBuilder fromHttpUrl(String httpUrl) throws InvalidUrlException {
return fromUriString(httpUrl);
}

private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) {
Expand Down
Loading

0 comments on commit f21e05a

Please sign in to comment.