This commit is contained in:
Looly 2022-04-08 21:36:45 +08:00
parent 8dbeb7c90f
commit 2b79119c00
8 changed files with 111 additions and 37 deletions

View File

@ -16,6 +16,7 @@
### 🐞Bug修复
* 【core 】 修复UserAgentUtil识别Linux出错issue#I50YGY@Gitee
* 【poi 】 修复ExcelWriter.getDisposition方法生成错误issue#2239@Github
* 【core 】 修复UrlBuilder重复编码的问题issue#2243@Github
-------------------------------------------------------------------------------------------------------------

View File

@ -1,5 +1,6 @@
package cn.hutool.core.codec;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
@ -150,9 +151,10 @@ public class PercentCodec implements Serializable {
*
* @param path 需要编码的字符串
* @param charset 编码, {@code null}返回原字符串表示不编码
* @param customSafeChar 自定义安全字符
* @return 编码后的字符串
*/
public String encode(CharSequence path, Charset charset) {
public String encode(CharSequence path, Charset charset, char... customSafeChar) {
if (null == charset || StrUtil.isEmpty(path)) {
return StrUtil.str(path);
}
@ -161,18 +163,18 @@ public class PercentCodec implements Serializable {
final ByteArrayOutputStream buf = new ByteArrayOutputStream();
final OutputStreamWriter writer = new OutputStreamWriter(buf, charset);
int c;
char c;
for (int i = 0; i < path.length(); i++) {
c = path.charAt(i);
if (safeCharacters.get(c)) {
rewrittenPath.append((char) c);
if (safeCharacters.get(c) || ArrayUtil.contains(customSafeChar, c)) {
rewrittenPath.append(c);
} else if (encodeSpaceAsPlus && c == CharUtil.SPACE) {
// 对于空格单独处理
rewrittenPath.append('+');
} else {
// convert to external encoding before hex conversion
try {
writer.write((char) c);
writer.write(c);
writer.flush();
} catch (IOException e) {
buf.reset();

View File

@ -59,6 +59,11 @@ public final class UrlBuilder implements Builder<String> {
* 编码用于URLEncode和URLDecode
*/
private Charset charset;
/**
* 是否需要编码`%`<br>
* 区别对待如果是则生成URL时需要重新全部编码否则跳过所有`%`
*/
private boolean needEncodePercent;
/**
* 使用URI构建UrlBuilder
@ -203,7 +208,7 @@ public final class UrlBuilder implements Builder<String> {
* @param path 路径例如/aa/bb/cc
* @param query 查询例如a=1&amp;b=2
* @param fragment 标识符例如#后边的部分
* @param charset 编码用于URLEncode和URLDecode
* @param charset 编码用于URLEncode和URLDecode{@code null}表示不编码
*/
public UrlBuilder(String scheme, String host, int port, UrlPath path, UrlQuery query, String fragment, Charset charset) {
this.charset = charset;
@ -213,6 +218,8 @@ public final class UrlBuilder implements Builder<String> {
this.path = path;
this.query = query;
this.setFragment(fragment);
// 编码非空情况下做解码
this.needEncodePercent = null != charset;
}
/**
@ -308,7 +315,7 @@ public final class UrlBuilder implements Builder<String> {
* @return 路径例如/aa/bb/cc
*/
public String getPathStr() {
return null == this.path ? StrUtil.SLASH : this.path.build(charset);
return null == this.path ? StrUtil.SLASH : this.path.build(charset, this.needEncodePercent);
}
/**
@ -378,7 +385,7 @@ public final class UrlBuilder implements Builder<String> {
* @return 查询语句例如a=1&amp;b=2
*/
public String getQueryStr() {
return null == this.query ? null : this.query.build(this.charset);
return null == this.query ? null : this.query.build(this.charset, this.needEncodePercent);
}
/**
@ -426,7 +433,8 @@ public final class UrlBuilder implements Builder<String> {
* @return 标识符例如#后边的部分
*/
public String getFragmentEncoded() {
return RFC3986.FRAGMENT.encode(this.fragment, this.charset);
final char[] safeChars = this.needEncodePercent ? null : new char[]{'%'};
return RFC3986.FRAGMENT.encode(this.fragment, this.charset, safeChars);
}
/**

View File

@ -126,19 +126,35 @@ public class UrlPath {
* @return 如果没有任何内容则返回空字符串""
*/
public String build(Charset charset) {
return build(charset, true);
}
/**
* 构建path前面带'/'<br>
* <pre>
* path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty
* </pre>
*
* @param charset encode编码null表示不做encode
* @param encodePercent 是否编码`%`
* @return 如果没有任何内容则返回空字符串""
* @since 5.8.0
*/
public String build(Charset charset, boolean encodePercent) {
if (CollUtil.isEmpty(this.segments)) {
return StrUtil.EMPTY;
}
final char[] safeChars = encodePercent ? null : new char[]{'%'};
final StringBuilder builder = new StringBuilder();
for (String segment : segments) {
if(builder.length() == 0){
// 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义
// path的第一部分不允许有":"其余部分允许
// 在此处的Path部分特指host之后的部分即不包含第一部分
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset));
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset, safeChars));
} else {
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset));
builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset, safeChars));
}
}
if (StrUtil.isEmpty(builder)) {

View File

@ -113,7 +113,7 @@ public class UrlQuery {
/**
* 构造
*
* @param queryMap 初始化的查询键值对
* @param queryMap 初始化的查询键值对
*/
public UrlQuery(Map<? extends CharSequence, ?> queryMap) {
this(queryMap, false);
@ -234,11 +234,7 @@ public class UrlQuery {
* @return URL查询字符串
*/
public String build(Charset charset) {
if (isFormUrlEncoded) {
return build(FormUrlencoded.ALL, FormUrlencoded.ALL, charset);
}
return build(RFC3986.QUERY_PARAM_NAME, RFC3986.QUERY_PARAM_VALUE, charset);
return build(charset, true);
}
/**
@ -249,17 +245,57 @@ public class UrlQuery {
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param keyCoder 键值对中键的编码器
* @param valueCoder 键值对中值的编码器
* @param charset encode编码null表示不做encode编码
* @param charset encode编码null表示不做encode编码
* @param encodePercent 是否编码`%`
* @return URL查询字符串
*/
public String build(Charset charset, boolean encodePercent) {
if (isFormUrlEncoded) {
return build(FormUrlencoded.ALL, FormUrlencoded.ALL, charset, encodePercent);
}
return build(RFC3986.QUERY_PARAM_NAME, RFC3986.QUERY_PARAM_VALUE, charset, encodePercent);
}
/**
* 构建URL查询字符串即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式<br>
* 对于{@code null}处理规则如下
* <ul>
* <li>如果key为{@code null}则这个键值对忽略</li>
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param keyCoder 键值对中键的编码器
* @param valueCoder 键值对中值的编码器
* @param charset encode编码null表示不做encode编码
* @return URL查询字符串
* @since 5.7.16
*/
public String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset) {
return build(keyCoder, valueCoder, charset, true);
}
/**
* 构建URL查询字符串即将key-value键值对转换为{@code key1=v1&key2=v2&key3=v3}形式<br>
* 对于{@code null}处理规则如下
* <ul>
* <li>如果key为{@code null}则这个键值对忽略</li>
* <li>如果value为{@code null}只保留key如key1对应value为{@code null}生成类似于{@code key1&key2=v2}形式</li>
* </ul>
*
* @param keyCoder 键值对中键的编码器
* @param valueCoder 键值对中值的编码器
* @param charset encode编码null表示不做encode编码
* @param encodePercent 是否编码`%`
* @return URL查询字符串
* @since 5.8.0
*/
public String build(PercentCodec keyCoder, PercentCodec valueCoder, Charset charset, boolean encodePercent) {
if (MapUtil.isEmpty(this.query)) {
return StrUtil.EMPTY;
}
final char[] safeChars = encodePercent ? null : new char[]{'%'};
final StringBuilder sb = new StringBuilder();
CharSequence name;
CharSequence value;
@ -269,10 +305,10 @@ public class UrlQuery {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(keyCoder.encode(name, charset));
sb.append(keyCoder.encode(name, charset, safeChars));
value = entry.getValue();
if (null != value) {
sb.append("=").append(valueCoder.encode(value, charset));
sb.append("=").append(valueCoder.encode(value, charset, safeChars));
}
}
}

View File

@ -14,4 +14,16 @@ public class RFC3986Test {
encode = RFC3986.QUERY_PARAM_VALUE.encode("a+1=b", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a+1=b", encode);
}
@Test
public void encodeQueryPercentTest(){
String encode = RFC3986.QUERY_PARAM_VALUE.encode("a=%b", CharsetUtil.CHARSET_UTF_8);
Assert.assertEquals("a=%25b", encode);
}
@Test
public void encodeQueryWithSafeTest(){
String encode = RFC3986.QUERY_PARAM_VALUE.encode("a=%25", CharsetUtil.CHARSET_UTF_8, '%');
Assert.assertEquals("a=%25", encode);
}
}

View File

@ -405,4 +405,18 @@ public class UrlBuilderTest {
params.forEach(builder::addQuery);
Assert.assertEquals("http://127.0.0.1/devicerecord/list?start=2022-03-31%2000:00:00&end=2022-03-31%2023:59:59&page=1&limit=10", builder.toString());
}
@Test
public void issue2242Test(){
}
@Test
public void issue2243Test(){
// https://github.com/dromara/hutool/issues/2243
// 如果用户已经做了%编码不应该重复编码
String url = "https://hutool.cn/v1.0?privateNum=%2B8616512884988";
final String s = UrlBuilder.of(url, null).setCharset(CharsetUtil.CHARSET_UTF_8).toString();
Assert.assertEquals(url, s);
}
}

View File

@ -193,19 +193,4 @@ public class HttpRequestTest {
HttpRequest request =HttpUtil.createGet(url).form(map);
Console.log(request.execute().body());
}
@Test
public void issueI50NHQTest(){
String url = "http://127.0.0.1/devicerecord/list";
HashMap<String, Object> params = new HashMap<>();
params.put("start", "2022-03-31 00:00:00");
params.put("end", "2022-03-31 23:59:59");
params.put("page", 1);
params.put("limit", 10);
String result = HttpRequest.get(url)
.header("token", "123")
.form(params).toString();
Console.log(result);
}
}