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");
     }

Reply via email to