This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 7b236d476 UnicodeUnescaper.unescapeJava/EcmaScript/Json: malformed \u
sequences throw undeclared IllegalArgumentException, AND non-ASCII digit
spellings of \u escapes are silently accepted; both arms of one missing
ASCII-hex prescan (f004).
7b236d476 is described below
commit 7b236d476e5c3f79063b0334d62744d4598e1dfd
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 21:52:38 2026 -0400
UnicodeUnescaper.unescapeJava/EcmaScript/Json: malformed \u sequences
throw undeclared IllegalArgumentException, AND non-ASCII digit spellings
of \u escapes are silently accepted; both arms of one missing ASCII-hex
prescan (f004).
---
src/changes/changes.xml | 1 +
.../lang3/text/translate/UnicodeUnescaper.java | 30 +++++++++++++---------
.../commons/lang3/StringEscapeUtilsTest.java | 4 ++-
.../lang3/text/translate/UnicodeUnescaperTest.java | 22 ++++++++++++----
4 files changed, 39 insertions(+), 18 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 77d7354d7..55040ec91 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -252,6 +252,7 @@ java.lang.NullPointerException: Cannot invoke
<action type="fix" dev="ggregory" due-to="Gaurav Pandey,
Gary Gregory">Align ReflectionDiffBuilder with AbstractReflection and add cycle
detection to prevent StackOverflowError on cyclic object graphs.</action>
<action type="fix" dev="ggregory" due-to="gaurav kumar
pandey, Gary Gregory">Fix DurationFormatUtils.formatPeriod() calculation when
pattern omits 'M' (#1780).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">FastDateParser parses 'Y' (week year) as plain calendar year;
asymmetric with FastDatePrinter and with SimpleDateFormat; boundary dates shift
by a full year, silently (f002).</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">UnicodeUnescaper.unescapeJava/EcmaScript/Json: malformed \u sequences
throw undeclared IllegalArgumentException, AND non-ASCII digit spellings of \u
escapes are silently accepted; both arms of one missing ASCII-hex prescan
(f004).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git
a/src/main/java/org/apache/commons/lang3/text/translate/UnicodeUnescaper.java
b/src/main/java/org/apache/commons/lang3/text/translate/UnicodeUnescaper.java
index 45f6fa544..9e211ed02 100644
---
a/src/main/java/org/apache/commons/lang3/text/translate/UnicodeUnescaper.java
+++
b/src/main/java/org/apache/commons/lang3/text/translate/UnicodeUnescaper.java
@@ -20,8 +20,14 @@
import java.io.IOException;
import java.io.Writer;
+import org.apache.commons.lang3.CharUtils;
+
/**
* Translates escaped Unicode values of the form \\u+\d\d\d\d back to Unicode.
It supports multiple 'u' characters and will work with or without the +.
+ * <p>
+ * Only ASCII hexadecimal digits ({@code [0-9a-fA-F]}) are accepted in the
four-digit value. Malformed sequences - non-ASCII-hex digits, sign characters,
+ * or an escape truncated by the end of the input - are not translated and
pass through unchanged.
+ * </p>
*
* @since 3.0
* @deprecated As of <a
href="https://commons.apache.org/proper/commons-lang/changes-report.html#a3.6">3.6</a>,
use Apache Commons Text
@@ -55,21 +61,21 @@ public int translate(final CharSequence input, final int
index, final Writer out
if (index + i + 4 <= input.length()) {
// Get 4 hex digits
final CharSequence unicode = input.subSequence(index + i,
index + i + 4);
- final char firstChar = unicode.charAt(0);
- if (firstChar == '+' || firstChar == '-') {
- // Integer.parseInt accepts a leading sign, but a Unicode
value is unsigned hex.
- throw new IllegalArgumentException("Sign character in
unicode value: '" + unicode + "'");
- }
- try {
- final int value = Integer.parseInt(unicode.toString(), 16);
- out.write((char) value);
- } catch (final NumberFormatException nfe) {
- throw new IllegalArgumentException("Unable to parse
unicode value: " + unicode, nfe);
+ // Pre-validate that all four characters are ASCII hexadecimal
digits, mirroring
+ // NumericEntityUnescaper's CharUtils.isHex discipline.
Integer.parseInt is looser than
+ // the \\uXXXX format: it accepts a leading sign and, via
Character.digit, decimal digits
+ // from any Unicode script and fullwidth Latin hex letters.
Anything that is not a
+ // well-formed escape is not translated and passes through
verbatim.
+ for (int j = 0; j < 4; j++) {
+ if (!CharUtils.isHex(unicode.charAt(j))) {
+ return 0;
+ }
}
+ out.write((char) Integer.parseInt(unicode.toString(), 16));
return i + 4;
}
- throw new IllegalArgumentException(
- "Less than 4 hex digits in unicode value: '" +
input.subSequence(index, input.length()) + "' due to end of CharSequence");
+ // Truncated escape at the end of the input: not a well-formed
escape, pass through verbatim.
+ return 0;
}
return 0;
}
diff --git a/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
b/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
index 64a4d1c1e..b4e87000c 100644
--- a/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringEscapeUtilsTest.java
@@ -473,7 +473,9 @@ void testUnescapeJava() throws IOException {
assertNull(StringEscapeUtils.unescapeJava(null));
assertNullPointerException(() ->
StringEscapeUtils.UNESCAPE_JAVA.translate(null, null));
assertNullPointerException(() ->
StringEscapeUtils.UNESCAPE_JAVA.translate("", null));
- assertThrows(RuntimeException.class, () ->
StringEscapeUtils.unescapeJava("\\u02-3"));
+ // A malformed Unicode escape is not translated by the Unicode
unescaper; the aggregate's
+ // stray-backslash rule then drops the lone backslash (same as
unescapeJava("\\") == "").
+ assertEquals("u02-3", StringEscapeUtils.unescapeJava("\\u02-3"));
assertUnescapeJava("", "");
assertUnescapeJava("test", "test");
assertUnescapeJava("\ntest\b", "\\ntest\\b");
diff --git
a/src/test/java/org/apache/commons/lang3/text/translate/UnicodeUnescaperTest.java
b/src/test/java/org/apache/commons/lang3/text/translate/UnicodeUnescaperTest.java
index 2049548ac..1b2190218 100644
---
a/src/test/java/org/apache/commons/lang3/text/translate/UnicodeUnescaperTest.java
+++
b/src/test/java/org/apache/commons/lang3/text/translate/UnicodeUnescaperTest.java
@@ -18,7 +18,6 @@
package org.apache.commons.lang3.text.translate;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.commons.lang3.AbstractLangTest;
import org.junit.jupiter.api.Test;
@@ -32,16 +31,29 @@ class UnicodeUnescaperTest extends AbstractLangTest {
@Test
void testLessThanFour() {
final UnicodeUnescaper uu = new UnicodeUnescaper();
+ // A truncated escape is not a well-formed escape: it passes through
untranslated.
final String input = "\\0047\\u006";
- assertThrows(IllegalArgumentException.class, () ->
uu.translate(input), "A lack of digits in a Unicode escape sequence failed to
throw an exception");
+ assertEquals(input, uu.translate(input), "A truncated Unicode escape
sequence must pass through untranslated");
+ }
+
+ @Test
+ void testNonAsciiHexDigits() {
+ final UnicodeUnescaper uu = new UnicodeUnescaper();
+ // Integer.parseInt would accept Unicode decimal digits from any
script and fullwidth Latin hex
+ // letters via Character.digit; those spellings are not well-formed
escapes and pass through.
+ assertEquals("\\u\uFF10\uFF10\uFF12\uFF12",
uu.translate("\\u\uFF10\uFF10\uFF12\uFF12"),
+ "Fullwidth digit spellings must pass through untranslated");
+ assertEquals("\\u\u0660\u0660\u0664\u0661",
uu.translate("\\u\u0660\u0660\u0664\u0661"),
+ "Arabic-Indic digit spellings must pass through untranslated");
}
@Test
void testSignedValue() {
final UnicodeUnescaper uu = new UnicodeUnescaper();
- // Integer.parseInt accepts a leading sign, so these used to decode to
a bogus char instead of throwing.
- assertThrows(IllegalArgumentException.class, () ->
uu.translate("\\u-047"), "A signed Unicode escape sequence failed to throw an
exception");
- assertThrows(IllegalArgumentException.class, () ->
uu.translate("\\u++0047"), "A signed Unicode escape sequence failed to throw an
exception");
+ // Integer.parseInt accepts a leading sign, but a sign character is
not an ASCII hex digit:
+ // these are not well-formed escapes and pass through untranslated.
+ assertEquals("\\u-047", uu.translate("\\u-047"), "A signed Unicode
escape sequence must pass through untranslated");
+ assertEquals("\\u++0047", uu.translate("\\u++0047"), "A signed Unicode
escape sequence must pass through untranslated");
// The documented u+ notation is still accepted.
assertEquals("G", uu.translate("\\u+0047"), "Failed to unescape
Unicode characters with 'u+' notation");
}