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-codec.git


The following commit(s) were added to refs/heads/master by this push:
     new a3348063 Validate BinaryCodec input while preserving leading-bit 
truncation
a3348063 is described below

commit a3348063804da7997a34ebe91af436d195bb1fae
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 18 14:20:23 2026 -0700

    Validate BinaryCodec input while preserving leading-bit truncation
    
    Reject non-ASCII binary values with IllegalArgumentException, including
    values in discarded prefixes. Preserve existing method signatures and
    decoding behavior for valid nonaligned input. Document validation and
    truncation, and add regression tests for all decoding entry points.
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/codec/binary/BinaryCodec.java   | 54 +++++++++++++++++++---
 .../commons/codec/binary/BinaryCodecTest.java      | 36 +++++++++++++++
 3 files changed, 84 insertions(+), 7 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ef78a50f..b35320e7 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -62,6 +62,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Reject invalid 
GitIdentifiers tree entry names and file/directory name conflicts to prevent 
ambiguous tree serialization and colliding identifiers.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Hex decoding now 
accepts only ASCII hexadecimal characters (0-9, A-F, a-f). Previously accepted 
non-ASCII Unicode digits and fullwidth letters now cause DecoderException, 
including when supplied as UTF-8 bytes or ByteBuffers.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Restrict Hex 
decoding to ASCII hexadecimal characters.</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Validate 
BinaryCodec input while preserving leading-bit truncation.</action>
       <!-- ADD -->
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add and use 
PhoneticEngine.Builder and deprecate old constructors.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
BeiderMorseEncoder.Builder and deprecate old constructor.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/BinaryCodec.java 
b/src/main/java/org/apache/commons/codec/binary/BinaryCodec.java
index f81d4d8c..c9603c83 100644
--- a/src/main/java/org/apache/commons/codec/binary/BinaryCodec.java
+++ b/src/main/java/org/apache/commons/codec/binary/BinaryCodec.java
@@ -29,15 +29,11 @@ import org.apache.commons.codec.EncoderException;
  * This class is immutable and thread-safe.
  * </p>
  *
- * TODO: may want to add more bit vector functions like and/or/xor/nand TODO: 
also might be good to generate boolean[] from byte[] et cetera.
- *
  * @since 1.3
  */
 public class BinaryCodec implements BinaryDecoder, BinaryEncoder {
 
-    /*
-     * tried to avoid using ArrayUtils to minimize dependencies while using 
these empty arrays - dep is just not worth it.
-     */
+    // TODO may want to add more bit vector functions like and/or/xor/nand 
TODO: also might be good to generate boolean[] from byte[] et cetera.
 
     /** Empty char array. */
     private static final char[] EMPTY_CHAR_ARRAY = {};
@@ -74,15 +70,28 @@ public class BinaryCodec implements BinaryDecoder, 
BinaryEncoder {
     /**
      * Decodes a byte array where each byte represents an ASCII '0' or '1'.
      *
+     * <p>
+     * All input must consist of ASCII {@code '0'} and {@code '1'} values. If 
the input length is not a multiple of 8, the leading
+     * {@code length % 8} values are validated but omitted from the decoded 
result. Null or empty input produces an empty byte array.
+     * </p>
+     *
      * @param ascii each byte represents an ASCII '0' or '1'.
      * @return The raw encoded binary where each bit corresponds to a byte in 
the byte array argument.
+     * @throws IllegalArgumentException if the input contains a value other 
than ASCII '0' or '1'.
      */
     public static byte[] fromAscii(final byte[] ascii) {
         if (isEmpty(ascii)) {
             return EMPTY_BYTE_ARRAY;
         }
         final int asciiLength = ascii.length;
-        // get length/8 times bytes with 3 bit shifts to the right of the 
length
+        // Validate all input, including leading values omitted from the 
decoded result.
+        for (int i = 0; i < asciiLength; i++) {
+            final byte b = ascii[i];
+            if (b != '0' && b != '1') {
+                throw new IllegalArgumentException("Input contains a value 
other than ASCII '0' or '1' at index " + i);
+            }
+        }
+        // Decode complete groups of 8, omitting any remaining leading values.
         final byte[] raw = new byte[asciiLength >> 3];
         /*
          * We decr index jj by 8 as we go along to not recompute indices using 
multiplication every time inside the loop.
@@ -100,15 +109,28 @@ public class BinaryCodec implements BinaryDecoder, 
BinaryEncoder {
     /**
      * Decodes a char array where each char represents an ASCII '0' or '1'.
      *
+     * <p>
+     * All input must consist of ASCII {@code '0'} and {@code '1'} values. If 
the input length is not a multiple of 8, the leading
+     * {@code length % 8} values are validated but omitted from the decoded 
result. Null or empty input produces an empty byte array.
+     * </p>
+     *
      * @param ascii each char represents an ASCII '0' or '1'.
      * @return The raw encoded binary where each bit corresponds to a char in 
the char array argument.
+     * @throws IllegalArgumentException if the input contains a value other 
than ASCII '0' or '1'.
      */
     public static byte[] fromAscii(final char[] ascii) {
         if (ascii == null || ascii.length == 0) {
             return EMPTY_BYTE_ARRAY;
         }
         final int asciiLength = ascii.length;
-        // get length/8 times bytes with 3 bit shifts to the right of the 
length
+        // Validate all input, including leading values omitted from the 
decoded result.
+        for (int i = 0; i < asciiLength; i++) {
+            final char c = ascii[i];
+            if (c != '0' && c != '1') {
+                throw new IllegalArgumentException("Input contains a value 
other than ASCII '0' or '1' at index " + i);
+            }
+        }
+        // Decode complete groups of 8, omitting any remaining leading values.
         final byte[] raw = new byte[asciiLength >> 3];
         /*
          * We decr index jj by 8 as we go along to not recompute indices using 
multiplication every time inside the loop.
@@ -212,8 +234,14 @@ public class BinaryCodec implements BinaryDecoder, 
BinaryEncoder {
     /**
      * Decodes a byte array where each byte represents an ASCII '0' or '1'.
      *
+     * <p>
+     * All input must consist of ASCII {@code '0'} and {@code '1'} values. If 
the input length is not a multiple of 8, the leading
+     * {@code length % 8} values are validated but omitted from the decoded 
result. Null or empty input produces an empty byte array.
+     * </p>
+     *
      * @param ascii each byte represents an ASCII '0' or '1'.
      * @return The raw encoded binary where each bit corresponds to a byte in 
the byte array argument.
+     * @throws IllegalArgumentException if the input contains a value other 
than ASCII '0' or '1'.
      * @see org.apache.commons.codec.Decoder#decode(Object)
      */
     @Override
@@ -224,8 +252,14 @@ public class BinaryCodec implements BinaryDecoder, 
BinaryEncoder {
     /**
      * Decodes a byte array where each byte represents an ASCII '0' or '1'.
      *
+     * <p>
+     * All input must consist of ASCII {@code '0'} and {@code '1'} values. If 
the input length is not a multiple of 8, the leading
+     * {@code length % 8} values are validated but omitted from the decoded 
result. Null or empty input produces an empty byte array.
+     * </p>
+     *
      * @param ascii each byte represents an ASCII '0' or '1'.
      * @return The raw encoded binary where each bit corresponds to a byte in 
the byte array argument.
+     * @throws IllegalArgumentException if the input contains a value other 
than ASCII '0' or '1'.
      * @throws DecoderException if argument is not a byte[], char[] or String.
      * @see org.apache.commons.codec.Decoder#decode(Object)
      */
@@ -277,8 +311,14 @@ public class BinaryCodec implements BinaryDecoder, 
BinaryEncoder {
     /**
      * Decodes a String where each char of the String represents an ASCII '0' 
or '1'.
      *
+     * <p>
+     * All input must consist of ASCII {@code '0'} and {@code '1'} values. If 
the input length is not a multiple of 8, the leading
+     * {@code length % 8} values are validated but omitted from the decoded 
result. Null or empty input produces an empty byte array.
+     * </p>
+     *
      * @param ascii String of '0' and '1' characters.
      * @return The raw encoded binary where each bit corresponds to a byte in 
the byte array argument.
+     * @throws IllegalArgumentException if the input contains a value other 
than ASCII '0' or '1'.
      * @see org.apache.commons.codec.Decoder#decode(Object)
      */
     public byte[] toByteArray(final String ascii) {
diff --git a/src/test/java/org/apache/commons/codec/binary/BinaryCodecTest.java 
b/src/test/java/org/apache/commons/codec/binary/BinaryCodecTest.java
index 7415da35..e5ddedd9 100644
--- a/src/test/java/org/apache/commons/codec/binary/BinaryCodecTest.java
+++ b/src/test/java/org/apache/commons/codec/binary/BinaryCodecTest.java
@@ -30,6 +30,8 @@ import org.apache.commons.codec.EncoderException;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * TestCase for BinaryCodec class.
@@ -189,6 +191,40 @@ class BinaryCodecTest {
         assertEquals(new String(bits), new String(decoded));
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = { "x", "x00000000", "0000000x", "00000002", 
"10x0zZ!1", "0000000 ", "0000000\n", "0000000\u0000",
+            "0000000\u00ff", "0000000\uff11", "0000000x00000000" })
+    void testDecodeInvalidInput(final String ascii) {
+        final byte[] bytes = ascii.getBytes(CHARSET_UTF8);
+        final char[] chars = ascii.toCharArray();
+        assertThrows(IllegalArgumentException.class, () -> 
BinaryCodec.fromAscii(bytes));
+        assertThrows(IllegalArgumentException.class, () -> 
BinaryCodec.fromAscii(chars));
+        assertThrows(IllegalArgumentException.class, () -> 
instance.decode(bytes));
+        assertThrows(IllegalArgumentException.class, () -> 
instance.decode((Object) bytes));
+        assertThrows(IllegalArgumentException.class, () -> 
instance.decode((Object) chars));
+        assertThrows(IllegalArgumentException.class, () -> 
instance.decode((Object) ascii));
+        assertThrows(IllegalArgumentException.class, () -> 
instance.toByteArray(ascii));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "1", "11", "111", "1111", "11111", "111111", 
"1111111" })
+    void testDecodeNonAlignedInput(final String prefix) throws 
DecoderException {
+        // Preserve truncation of valid leading bits, including inputs shorter 
than one byte.
+        assertArrayEquals(new byte[0], 
BinaryCodec.fromAscii(prefix.getBytes(CHARSET_UTF8)));
+        assertArrayEquals(new byte[0], 
BinaryCodec.fromAscii(prefix.toCharArray()));
+        final String ascii = prefix + "00000001";
+        final byte[] bytes = ascii.getBytes(CHARSET_UTF8);
+        final char[] chars = ascii.toCharArray();
+        final byte[] expected = { BIT_0 };
+        assertArrayEquals(expected, BinaryCodec.fromAscii(bytes));
+        assertArrayEquals(expected, BinaryCodec.fromAscii(chars));
+        assertArrayEquals(expected, instance.decode(bytes));
+        assertArrayEquals(expected, (byte[]) instance.decode((Object) bytes));
+        assertArrayEquals(expected, (byte[]) instance.decode((Object) chars));
+        assertArrayEquals(expected, (byte[]) instance.decode((Object) ascii));
+        assertArrayEquals(expected, instance.toByteArray(ascii));
+    }
+
     /**
      * Tests for Object decode(Object)
      */

Reply via email to