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 472a2894 [CODEC-342] Base32.Builder#setEncodeTable(...) can create an 
instance that cannot decode its own output (#436)
472a2894 is described below

commit 472a2894046e14fe2fd6e4f5312da8afb93c8769
Author: OldTruckDriver <[email protected]>
AuthorDate: Thu Jun 18 12:08:10 2026 +1000

    [CODEC-342] Base32.Builder#setEncodeTable(...) can create an instance that 
cannot decode its own output (#436)
    
    * [CODEC-343] Fix Base32 hex decode table builder
    
    Configure setHexDecodeTable(boolean) with the matching encode table instead 
of passing a decode lookup table to setEncodeTable(byte...).
    
    Add a regression test showing the configured codec encodes with the 
Base32-Hex alphabet and decodes its own output.
    
    Reviewed-by: OpenAI Codex
    Reviewed-by: Anthropic Claude Code
    
    * [CODEC-342] Fix Base32 custom alphabet decode table
    
    Derive Base32 decode tables from custom encode tables so a configured codec 
can decode its own output.
    
    Reject encode tables that do not contain exactly 32 unique byte values.
    
    Reviewed-by: OpenAI Codex
    Reviewed-by: Anthropic Claude Code
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
---
 src/changes/changes.xml                            |  2 +-
 .../org/apache/commons/codec/binary/Base32.java    | 63 ++++++++++++++++++++--
 .../apache/commons/codec/binary/Base32Test.java    | 39 ++++++++++++++
 3 files changed, 98 insertions(+), 6 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 3763d0b8..5d8022b8 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,7 +45,7 @@ The <action> type attribute can be add,update,fix,remove.
   <body>
     <release version="1.22.1" date="YYYY-MM-DD" description="This is a feature 
and maintenance release. Java 8 or later is required.">
       <!-- FIX -->
-      <action type="fix" issue="CODEC-343" dev="ggregory" due-to="Ruiqi Dong, 
Gary Gregory">Base32.Builder.setHexDecodeTable(boolean) sets the encode table 
to a decode lookup table.</action>
+      <action type="fix" issue="CODEC-342" dev="ggregory" due-to="Ruiqi Dong, 
Gary Gregory">Base32.Builder.setEncodeTable(byte...) can create a codec that 
cannot decode its own output.</action>
       <action type="fix" issue="CODEC-341" dev="ggregory" due-to="Ruiqi Dong, 
Gary Gregory">Base16.Builder.setEncodeTable(byte...) can create a codec that 
cannot decode its own output.</action>
       <action type="fix" issue="CODEC-339" dev="ggregory" due-to="Ruiqi Dong, 
Gary Gregory">URLCodec.encodeUrl(BitSet, byte[]) allows custom safe sets to 
emit URL encoding control characters.</action>
       <action type="fix" issue="CODEC-338" dev="ggregory" due-to="Ruiqi Dong, 
Gary Gregory">PercentCodec loses literal '+' when plusForSpace is 
enabled.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base32.java 
b/src/main/java/org/apache/commons/codec/binary/Base32.java
index a1206c67..19e15fc6 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base32.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base32.java
@@ -96,9 +96,19 @@ public class Base32 extends BaseNCodec {
             return new Base32(this);
         }
 
+        /**
+         * Sets the encode table and derives the matching decode table.
+         * <p>
+         * The RFC 4648 Base32 and Base32 Hex tables keep their 
case-insensitive decoders.
+         * </p>
+         *
+         * @param encodeTable the encode table with exactly 32 unique entries, 
null resets to the default.
+         * @return {@code this} instance.
+         * @throws IllegalArgumentException if the encode table does not 
contain exactly 32 unique entries.
+         */
         @Override
         public Builder setEncodeTable(final byte... encodeTable) {
-            super.setDecodeTableRaw(Arrays.equals(encodeTable, 
HEX_ENCODE_TABLE) ? HEX_DECODE_TABLE : DECODE_TABLE);
+            super.setDecodeTableRaw(toDecodeTable(encodeTable));
             return super.setEncodeTable(encodeTable);
         }
 
@@ -145,6 +155,8 @@ public class Base32 extends BaseNCodec {
 
     private static final int BYTES_PER_ENCODED_BLOCK = 8;
     private static final int BYTES_PER_UNENCODED_BLOCK = 5;
+    private static final int DECODING_TABLE_LENGTH = 256;
+    private static final int ENCODING_TABLE_LENGTH = 1 << 
BITS_PER_ENCODED_BYTE;
 
     /**
      * This array is a lookup table that translates Unicode characters drawn 
from the "Base32 Alphabet" (as specified in Table 3 of RFC 4648) into their 
5-bit
@@ -256,6 +268,29 @@ public class Base32 extends BaseNCodec {
         return new Builder();
     }
 
+    /**
+     * Calculates a decode table for a given encode table.
+     *
+     * @param encodeTable that is used to determine decode lookup table.
+     * @return A new decode table.
+     * @throws IllegalArgumentException if the encode table does not contain 
exactly 32 unique entries.
+     */
+    private static byte[] calculateDecodeTable(final byte[] encodeTable) {
+        if (encodeTable.length != ENCODING_TABLE_LENGTH) {
+            throw new IllegalArgumentException("encodeTable must have exactly 
32 entries.");
+        }
+        final byte[] decodeTable = new byte[DECODING_TABLE_LENGTH];
+        Arrays.fill(decodeTable, (byte) -1);
+        for (int i = 0; i < encodeTable.length; i++) {
+            final int encodedByte = encodeTable[i] & 0xff;
+            if (decodeTable[encodedByte] != -1) {
+                throw new IllegalArgumentException("encodeTable must not 
contain duplicate entries.");
+            }
+            decodeTable[encodedByte] = (byte) i;
+        }
+        return decodeTable;
+    }
+
     private static byte[] decodeTable(final boolean useHex) {
         return useHex ? HEX_DECODE_TABLE : DECODE_TABLE;
     }
@@ -276,6 +311,23 @@ public class Base32 extends BaseNCodec {
         return useHex ? HEX_ENCODE_TABLE : ENCODE_TABLE;
     }
 
+    /**
+     * Gets the decode table that matches the given encode table.
+     *
+     * @param encodeTable that is used to determine decode lookup table.
+     * @return the matching decode table.
+     */
+    private static byte[] toDecodeTable(final byte[] encodeTable) {
+        final byte[] table = encodeTable != null ? encodeTable : ENCODE_TABLE;
+        if (Arrays.equals(table, ENCODE_TABLE)) {
+            return DECODE_TABLE;
+        }
+        if (Arrays.equals(table, HEX_ENCODE_TABLE)) {
+            return HEX_DECODE_TABLE;
+        }
+        return calculateDecodeTable(table);
+    }
+
     /**
      * Convenience variable to help us determine when our buffer is going to 
run out of room and needs resizing. {@code encodeSize = {@link
      * #BYTES_PER_ENCODED_BLOCK} + lineSeparator.length;}
@@ -530,14 +582,14 @@ public class Base32 extends BaseNCodec {
         }
         final int decodeSize = this.encodeSize - 1;
         for (int i = 0; i < inAvail; i++) {
-            final byte b = input[inPos++];
-            if (b == pad) {
+            final int b = input[inPos++] & 0xff;
+            if (b == (pad & 0xff)) {
                 // We're done.
                 context.eof = true;
                 break;
             }
             final byte[] buffer = ensureBufferSize(decodeSize, context);
-            if (b >= 0 && b < this.decodeTable.length) {
+            if (b < this.decodeTable.length) {
                 final int result = this.decodeTable[b];
                 if (result >= 0) {
                     context.modulus = (context.modulus + 1) % 
BYTES_PER_ENCODED_BLOCK;
@@ -738,7 +790,8 @@ public class Base32 extends BaseNCodec {
      */
     @Override
     public boolean isInAlphabet(final byte octet) {
-        return isInAlphabet(octet, decodeTable);
+        final int value = octet & 0xff;
+        return value < decodeTable.length && decodeTable[value] != -1;
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/codec/binary/Base32Test.java 
b/src/test/java/org/apache/commons/codec/binary/Base32Test.java
index ed3e0b9c..dbc0ff3f 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base32Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base32Test.java
@@ -398,6 +398,45 @@ class Base32Test {
         assertEquals(CodecPolicy.LENIENT, 
Base32.builder().setDecodingPolicy(null).get().getCodecPolicy());
     }
 
+    @Test
+    void testBuilderCustomEncodeTableAffectsDecodeTable() {
+        final byte[] encodeTable = ENCODE_TABLE.clone();
+        final byte temp = encodeTable[0];
+        encodeTable[0] = encodeTable[1];
+        encodeTable[1] = temp;
+        final Base32 base32 = 
Base32.builder().setEncodeTable(encodeTable).setLineLength(0).get();
+        final byte[] data = { 0 };
+        final byte[] encoded = base32.encode(data);
+        assertEquals("BB======", new String(encoded, 
StandardCharsets.US_ASCII));
+        assertArrayEquals(data, base32.decode(encoded));
+    }
+
+    @Test
+    void testBuilderCustomEncodeTableRejectsDuplicateEntries() {
+        final byte[] encodeTable = ENCODE_TABLE.clone();
+        encodeTable[1] = encodeTable[0];
+        assertThrows(IllegalArgumentException.class, () -> 
Base32.builder().setEncodeTable(encodeTable));
+    }
+
+    @Test
+    void testBuilderCustomEncodeTableRejectsInvalidLength() {
+        assertThrows(IllegalArgumentException.class, () -> 
Base32.builder().setEncodeTable(Arrays.copyOf(ENCODE_TABLE, ENCODE_TABLE.length 
- 1)));
+    }
+
+    @Test
+    void testBuilderCustomEncodeTableWithNonAsciiBytes() {
+        final byte[] encodeTable = new byte[32];
+        for (int i = 0; i < encodeTable.length; i++) {
+            encodeTable[i] = (byte) (0x80 + i);
+        }
+        final Base32 base32 = 
Base32.builder().setEncodeTable(encodeTable).setLineLength(0).get();
+        final byte[] data = { 0 };
+        final byte[] encoded = base32.encode(data);
+        assertArrayEquals(new byte[] { (byte) 0x80, (byte) 0x80, '=', '=', 
'=', '=', '=', '=' }, encoded);
+        assertTrue(base32.isInAlphabet((byte) 0x80));
+        assertArrayEquals(data, base32.decode(encoded));
+    }
+
     @Test
     void testBuilderLineAttributes() {
         assertNull(Base32.builder().get().getLineSeparator());

Reply via email to