You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2026/09/05 01:52:45 UTC

(commons-lang) branch master updated: 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).

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 <ga...@gmail.com>
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");
     }