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)
*/