Compare commits

..

6 Commits

Author SHA1 Message Date
Looly
86c3c530b6 修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBufferposition,导致入参变化 (pr#4153@Github) 2025-11-25 18:54:13 +08:00
Looly
2a08f40527 BufferUtilTest 2025-11-25 18:53:23 +08:00
Golden Looly
a8cc614fa6
Merge pull request #4153 from ZhonglinGui/v5-dev
修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBuffer 的 position,导致入参变化
2025-11-25 18:52:00 +08:00
Will
a6151b6ee8 修复StrUtil.str(ByteBuffer, Charset) 方法修改入参 ByteBuffer 的 position,导致入参变化 2025-11-25 18:08:03 +08:00
Looly
3c067f1871 优化EscapeUtil,兼容不规范的转义(pr#4150@Github) 2025-11-25 17:07:17 +08:00
Looly
654db66ecb fix pr#4149@Github 2025-11-25 15:16:42 +08:00
9 changed files with 215 additions and 29 deletions

View File

@ -1,7 +1,7 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
# 5.8.42(2025-11-24)
# 5.8.42(2025-11-25)
### 🐣新特性
* 【core 】 `ListUtil`增加`zip`方法pr#4052@Github
@ -9,6 +9,7 @@
* 【ai 】 增加代理支持pr#4107@Github
* 【core 】 `CharSequenceUtil`增加`builder`方法重载pr#4107@Github
* 【core 】 `Combination``Arrangement `重构避免数组频繁拷贝并避免溢出pr#4144@Github
* 【core 】 优化`EscapeUtil`兼容不规范的转义pr#4150@Github
### 🐞Bug修复
* 【jwt 】 修复verify方法在定义alg为`none`时验证失效问题issue#4105@Github
@ -24,6 +25,11 @@
* 【core 】 修复`Validator.isBetween`在高精度Number类型下存在精度丢失问题pr#4136@Github
* 【core 】 修复`FileNameUtil.extName`在特殊后缀判断逻辑过于宽松导致误判问题pr#4142@Github
* 【core 】 修复`TypeUtil.getClass`无法识别`GenericArrayType`问题pr#4138@Github
* 【core 】 修复`CreditCodeUtil.randomCreditCode`部分字母未使用问题pr#4149@Github
* 【core 】 修复`CacheableAnnotationAttribute`可能并发问题pr#4149@Github
* 【core 】 修复`URLUtil.url`未断开连接问题pr#4149@Github
* 【core 】 修复`Bimap.put`重复put问题pr#4150@Github
* 【core 】 修复`StrUtil.str(ByteBuffer, Charset)` 方法修改入参 `ByteBuffer``position`,导致入参变化 pr#4153@Github
-------------------------------------------------------------------------------------------------------------
# 5.8.41(2025-10-12)

View File

@ -15,7 +15,7 @@ import java.lang.reflect.Method;
public class CacheableAnnotationAttribute implements AnnotationAttribute {
private volatile boolean valueInvoked;
private Object value;
private volatile Object value;
private boolean defaultValueInvoked;
private Object defaultValue;

View File

@ -39,7 +39,7 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
}
this.inverse.put(value, key);
}
return super.put(key, value);
return oldValue;
}
@Override
@ -94,10 +94,12 @@ public class BiMap<K, V> extends MapWrapper<K, V> {
@Override
public V putIfAbsent(K key, V value) {
if (null != this.inverse) {
this.inverse.putIfAbsent(value, key);
final V oldValue = super.putIfAbsent(key, value);
// 只有当oldValue为null时(即key之前不存在),才更新反向Map
if (null == oldValue && null != this.inverse) {
this.inverse.put(value, key);
}
return super.putIfAbsent(key, value);
return oldValue;
}
@Override

View File

@ -100,7 +100,7 @@ public class CreditCodeUtil {
//
for (int i = 0; i < 2; i++) {
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1);
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);
buf.append(Character.toUpperCase(BASE_CODE_ARRAY[num]));
}
for (int i = 2; i < 8; i++) {
@ -108,7 +108,7 @@ public class CreditCodeUtil {
buf.append(BASE_CODE_ARRAY[num]);
}
for (int i = 8; i < 17; i++) {
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length - 1);
int num = RandomUtil.randomInt(BASE_CODE_ARRAY.length);
buf.append(BASE_CODE_ARRAY[num]);
}

View File

@ -10,7 +10,7 @@ import cn.hutool.core.text.escape.XmlUnescape;
* 转义和反转义工具类Escape / Unescape<br>
* escape采用ISO Latin字符集对指定的字符串进行编码<br>
* 所有的空格符标点符号特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)
* TODO 6.x迁移到core.text.escape包下
* TODO 7.x迁移到core.text.escape包下
*
* @author xiaoleilu
*/
@ -20,11 +20,11 @@ public class EscapeUtil {
* 不转义的符号编码
*/
private static final String NOT_ESCAPE_CHARS = "*@-_+./";
private static final Filter<Character> JS_ESCAPE_FILTER = c -> false == (
Character.isDigit(c)
|| Character.isLowerCase(c)
|| Character.isUpperCase(c)
|| StrUtil.contains(NOT_ESCAPE_CHARS, c)
private static final Filter<Character> JS_ESCAPE_FILTER = c -> !(
Character.isDigit(c)
|| Character.isLowerCase(c)
|| Character.isUpperCase(c)
|| StrUtil.contains(NOT_ESCAPE_CHARS, c)
);
/**
@ -122,7 +122,7 @@ public class EscapeUtil {
char c;
for (int i = 0; i < content.length(); i++) {
c = content.charAt(i);
if (false == filter.accept(c)) {
if (!filter.accept(c)) {
tmp.append(c);
} else if (c < 256) {
tmp.append("%");
@ -143,36 +143,69 @@ public class EscapeUtil {
}
/**
* Escape解码
* Escape解码支持两种转义格式的解码
* <ul>
* <li>%XX - 两位十六进制数字用于表示ASCII字符0-255</li>
* <li>%uXXXX - 四位十六进制数字用于表示Unicode字符</li>
* </ul>
* <p>
* 对于不完整的转义序列本方法会将其原样保留而不抛出异常
* <ul>
* <li>字符串末尾的单独"%"字符会被原样保留</li>
* <li>"%u"后面不足4位十六进制数字时整个不完整序列会被原样保留</li>
* <li>"%"后面不足2位十六进制数字时%u格式整个不完整序列会被原样保留</li>
* </ul>
* 例如
* <pre>
* unescape("test%") = "test%" // 末尾的%被保留
* unescape("test%u12") = "test%u12" // 不足4位原样保留
* unescape("test%2") = "test%2" // 不足2位原样保留
* unescape("test%20") = "test " // 正常解码空格
* unescape("test%u4E2D") = "test中" // 正常解码中文字符
* </pre>
*
* @param content 被转义的内容
* @return 解码后的字符串
*/
public static String unescape(String content) {
public static String unescape(final String content) {
if (StrUtil.isBlank(content)) {
return content;
}
StringBuilder tmp = new StringBuilder(content.length());
final int len = content.length();
final StringBuilder tmp = new StringBuilder(len);
int lastPos = 0;
int pos;
char ch;
while (lastPos < content.length()) {
while (lastPos < len) {
pos = content.indexOf("%", lastPos);
if (pos == lastPos) {
if (content.charAt(pos + 1) == 'u') {
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
tmp.append(ch);
lastPos = pos + 6;
if (pos + 1 < len && content.charAt(pos + 1) == 'u') {
if (pos + 6 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
tmp.append(ch);
lastPos = pos + 6;
} else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
} else {
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
tmp.append(ch);
lastPos = pos + 3;
// Check if there's enough characters for hex escape (%XX)
if (pos + 3 <= len) {
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
tmp.append(ch);
lastPos = pos + 3;
} else {
// Not enough characters, append as-is
tmp.append(content.substring(pos));
lastPos = len;
}
}
} else {
if (pos == -1) {
tmp.append(content.substring(lastPos));
lastPos = content.length();
lastPos = len;
} else {
tmp.append(content, lastPos, pos);
lastPos = pos;

View File

@ -261,7 +261,7 @@ public class StrUtil extends CharSequenceUtil implements StrPool {
if (null == charset) {
charset = Charset.defaultCharset();
}
return charset.decode(data).toString();
return charset.decode(data.duplicate()).toString();
}
/**

View File

@ -807,8 +807,9 @@ public class URLUtil extends URLEncodeUtil {
} else {
// 如果资源打在jar包中或来自网络使用网络请求长度
// issue#3226, 来自Spring的AbstractFileResolvingResource
URLConnection con = null;
try {
final URLConnection con = url.openConnection();
con = url.openConnection();
useCachesIfNecessary(con);
if (con instanceof HttpURLConnection) {
final HttpURLConnection httpCon = (HttpURLConnection) con;
@ -817,6 +818,10 @@ public class URLUtil extends URLEncodeUtil {
return con.getContentLengthLong();
} catch (final IOException e) {
throw new IORuntimeException(e);
} finally {
if (con instanceof HttpURLConnection) {
((HttpURLConnection) con).disconnect();
}
}
}
}

View File

@ -1,6 +1,7 @@
package cn.hutool.core.io;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
@ -63,4 +64,21 @@ public class BufferUtilTest {
// 读取剩余部分
assertEquals("cc", StrUtil.utf8Str(BufferUtil.readBytes(buffer)));
}
@Test
public void testByteBufferSideEffect() {
String originalText = "Hello";
ByteBuffer buffer = ByteBuffer.wrap(originalText.getBytes(StandardCharsets.UTF_8));
// 此时 buffer.remaining() == 5
assertEquals(5, buffer.remaining());
// 调用工具类转换打印buffer内容
String result = StrUtil.str(buffer, StandardCharsets.UTF_8);
assertEquals(originalText, result);
// 预期
// 工具类不应该修改原 buffer 的指针remaining 应该依然为 5
// 再次调用工具类转换输出结果应该不变
assertEquals(originalText, StrUtil.str(buffer, StandardCharsets.UTF_8));
}
}

View File

@ -66,4 +66,126 @@ public class EscapeUtilTest {
final String s = EscapeUtil.unescapeHtml4(str);
assertEquals("'some text with single quotes'", s);
}
@Test
public void escapeXmlTest(){
final String a = "<>";
final String escape = EscapeUtil.escapeXml(a);
assertEquals("&lt;&gt;", escape);
assertEquals("中文“双引号”", EscapeUtil.escapeXml("中文“双引号”"));
}
@Test
void testUnescapeNull() {
assertNull(EscapeUtil.unescape(null));
}
@Test
void testUnescapeEmpty() {
assertEquals("", EscapeUtil.unescape(""));
}
@Test
void testUnescapeBlank() {
assertEquals(" ", EscapeUtil.unescape(" "));
}
@Test
void testUnescapeAsciiCharacters() {
// 测试ASCII字符转义
assertEquals("hello", EscapeUtil.unescape("hello"));
assertEquals("test space", EscapeUtil.unescape("test%20space"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("a", EscapeUtil.unescape("%61"));
assertEquals("0", EscapeUtil.unescape("%30"));
assertEquals("!", EscapeUtil.unescape("%21"));
assertEquals("@", EscapeUtil.unescape("%40"));
assertEquals("#", EscapeUtil.unescape("%23"));
}
@Test
void testUnescapeUnicodeCharacters() {
// 测试Unicode字符转义
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u6587"));
assertEquals("", EscapeUtil.unescape("%u6D4B"));
assertEquals("", EscapeUtil.unescape("%u8BD5"));
assertEquals("😊", EscapeUtil.unescape("%uD83D%uDE0A")); // 笑脸表情
}
@Test
void testUnescapeMixedContent() {
// 测试混合内容
assertEquals("Hello 世界!", EscapeUtil.unescape("Hello%20%u4E16%u754C%21"));
assertEquals("测试: 100%", EscapeUtil.unescape("%u6D4B%u8BD5%3A%20100%25"));
assertEquals("a+b=c", EscapeUtil.unescape("a%2Bb%3Dc"));
}
@Test
void testUnescapeIncompleteEscapeSequences() {
// 测试不完整的转义序列
assertEquals("test%", EscapeUtil.unescape("test%"));
assertEquals("test%u", EscapeUtil.unescape("test%u"));
assertEquals("test%u1", EscapeUtil.unescape("test%u1"));
assertEquals("test%u12", EscapeUtil.unescape("test%u12"));
assertEquals("test%u123", EscapeUtil.unescape("test%u123"));
assertEquals("test%1", EscapeUtil.unescape("test%1"));
assertEquals("test%2", EscapeUtil.unescape("test%2"));
}
@Test
void testUnescapeEdgeCases() {
// 测试边界情况
assertEquals("%", EscapeUtil.unescape("%"));
assertEquals("%u", EscapeUtil.unescape("%u"));
assertEquals("%%", EscapeUtil.unescape("%%"));
assertEquals("%u%", EscapeUtil.unescape("%u%"));
assertEquals("100% complete", EscapeUtil.unescape("100%25%20complete"));
}
@Test
void testUnescapeMultipleEscapeSequences() {
// 测试多个连续的转义序列
assertEquals("ABC", EscapeUtil.unescape("%41%42%43"));
assertEquals("中文测试", EscapeUtil.unescape("%u4E2D%u6587%u6D4B%u8BD5"));
assertEquals("A 中 B", EscapeUtil.unescape("%41%20%u4E2D%20%42"));
}
@Test
void testUnescapeSpecialCharacters() {
// 测试特殊字符
assertEquals("\n", EscapeUtil.unescape("%0A"));
assertEquals("\r", EscapeUtil.unescape("%0D"));
assertEquals("\t", EscapeUtil.unescape("%09"));
assertEquals(" ", EscapeUtil.unescape("%20"));
assertEquals("<", EscapeUtil.unescape("%3C"));
assertEquals(">", EscapeUtil.unescape("%3E"));
assertEquals("&", EscapeUtil.unescape("%26"));
}
@Test
void testUnescapeComplexScenario() {
// 测试复杂场景
final String original = "Hello 世界! 这是测试。Email: test@example.com";
final String escaped = "Hello%20%u4E16%u754C%21%20%u8FD9%u662F%u6D4B%u8BD5%u3002Email%3A%20test%40example.com";
assertEquals(original, EscapeUtil.unescape(escaped));
}
@Test
void testUnescapeWithIncompleteAtEnd() {
// 测试末尾有不完整转义序列
assertEquals("normal%", EscapeUtil.unescape("normal%"));
assertEquals("normal%u", EscapeUtil.unescape("normal%u"));
assertEquals("normal%u1", EscapeUtil.unescape("normal%u1"));
assertEquals("normal%1", EscapeUtil.unescape("normal%1"));
}
@Test
void testUnescapeUppercaseHex() {
// 测试大写十六进制
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("A", EscapeUtil.unescape("%41"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
assertEquals("", EscapeUtil.unescape("%u4E2D"));
}
}