Skip to content

Commit faa5b84

Browse files
[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
1 parent 7dcaa2a commit faa5b84

3 files changed

Lines changed: 98 additions & 5 deletions

File tree

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The <action> type attribute can be add,update,fix,remove.
4545
<body>
4646
<release version="1.22.1" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required.">
4747
<!-- FIX -->
48+
<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>
4849
<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>
4950
<action type="add" issue="CODEC-337" dev="pkarwasz" due-to="Ruiqi Dong, Gary Gregory">Digest ALL reuses System.in, so only the first algorithm sees the real input (#431).</action>
5051
<!-- ADD -->

src/main/java/org/apache/commons/codec/binary/Base32.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,19 @@ public Base32 get() {
9696
return new Base32(this);
9797
}
9898

99+
/**
100+
* Sets the encode table and derives the matching decode table.
101+
* <p>
102+
* The RFC 4648 Base32 and Base32 Hex tables keep their case-insensitive decoders.
103+
* </p>
104+
*
105+
* @param encodeTable the encode table with exactly 32 unique entries, null resets to the default.
106+
* @return {@code this} instance.
107+
* @throws IllegalArgumentException if the encode table does not contain exactly 32 unique entries.
108+
*/
99109
@Override
100110
public Builder setEncodeTable(final byte... encodeTable) {
101-
super.setDecodeTableRaw(Arrays.equals(encodeTable, HEX_ENCODE_TABLE) ? HEX_DECODE_TABLE : DECODE_TABLE);
111+
super.setDecodeTableRaw(toDecodeTable(encodeTable));
102112
return super.setEncodeTable(encodeTable);
103113
}
104114

@@ -145,6 +155,8 @@ public Builder setHexEncodeTable(final boolean useHex) {
145155

146156
private static final int BYTES_PER_ENCODED_BLOCK = 8;
147157
private static final int BYTES_PER_UNENCODED_BLOCK = 5;
158+
private static final int DECODING_TABLE_LENGTH = 256;
159+
private static final int ENCODING_TABLE_LENGTH = 1 << BITS_PER_ENCODED_BYTE;
148160

149161
/**
150162
* 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 static Builder builder() {
256268
return new Builder();
257269
}
258270

271+
/**
272+
* Calculates a decode table for a given encode table.
273+
*
274+
* @param encodeTable that is used to determine decode lookup table.
275+
* @return A new decode table.
276+
* @throws IllegalArgumentException if the encode table does not contain exactly 32 unique entries.
277+
*/
278+
private static byte[] calculateDecodeTable(final byte[] encodeTable) {
279+
if (encodeTable.length != ENCODING_TABLE_LENGTH) {
280+
throw new IllegalArgumentException("encodeTable must have exactly 32 entries.");
281+
}
282+
final byte[] decodeTable = new byte[DECODING_TABLE_LENGTH];
283+
Arrays.fill(decodeTable, (byte) -1);
284+
for (int i = 0; i < encodeTable.length; i++) {
285+
final int encodedByte = encodeTable[i] & 0xff;
286+
if (decodeTable[encodedByte] != -1) {
287+
throw new IllegalArgumentException("encodeTable must not contain duplicate entries.");
288+
}
289+
decodeTable[encodedByte] = (byte) i;
290+
}
291+
return decodeTable;
292+
}
293+
259294
private static byte[] decodeTable(final boolean useHex) {
260295
return useHex ? HEX_DECODE_TABLE : DECODE_TABLE;
261296
}
@@ -276,6 +311,23 @@ private static byte[] encodeTable(final boolean useHex) {
276311
return useHex ? HEX_ENCODE_TABLE : ENCODE_TABLE;
277312
}
278313

314+
/**
315+
* Gets the decode table that matches the given encode table.
316+
*
317+
* @param encodeTable that is used to determine decode lookup table.
318+
* @return the matching decode table.
319+
*/
320+
private static byte[] toDecodeTable(final byte[] encodeTable) {
321+
final byte[] table = encodeTable != null ? encodeTable : ENCODE_TABLE;
322+
if (Arrays.equals(table, ENCODE_TABLE)) {
323+
return DECODE_TABLE;
324+
}
325+
if (Arrays.equals(table, HEX_ENCODE_TABLE)) {
326+
return HEX_DECODE_TABLE;
327+
}
328+
return calculateDecodeTable(table);
329+
}
330+
279331
/**
280332
* Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. {@code encodeSize = {@link
281333
* #BYTES_PER_ENCODED_BLOCK} + lineSeparator.length;}
@@ -530,14 +582,14 @@ void decode(final byte[] input, int inPos, final int inAvail, final Context cont
530582
}
531583
final int decodeSize = this.encodeSize - 1;
532584
for (int i = 0; i < inAvail; i++) {
533-
final byte b = input[inPos++];
534-
if (b == pad) {
585+
final int b = input[inPos++] & 0xff;
586+
if (b == (pad & 0xff)) {
535587
// We're done.
536588
context.eof = true;
537589
break;
538590
}
539591
final byte[] buffer = ensureBufferSize(decodeSize, context);
540-
if (b >= 0 && b < this.decodeTable.length) {
592+
if (b < this.decodeTable.length) {
541593
final int result = this.decodeTable[b];
542594
if (result >= 0) {
543595
context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK;
@@ -738,7 +790,8 @@ byte[] getLineSeparator() {
738790
*/
739791
@Override
740792
public boolean isInAlphabet(final byte octet) {
741-
return isInAlphabet(octet, decodeTable);
793+
final int value = octet & 0xff;
794+
return value < decodeTable.length && decodeTable[value] != -1;
742795
}
743796

744797
/**

src/test/java/org/apache/commons/codec/binary/Base32Test.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,45 @@ void testBuilderCodecPolicy() {
398398
assertEquals(CodecPolicy.LENIENT, Base32.builder().setDecodingPolicy(null).get().getCodecPolicy());
399399
}
400400

401+
@Test
402+
void testBuilderCustomEncodeTableAffectsDecodeTable() {
403+
final byte[] encodeTable = ENCODE_TABLE.clone();
404+
final byte temp = encodeTable[0];
405+
encodeTable[0] = encodeTable[1];
406+
encodeTable[1] = temp;
407+
final Base32 base32 = Base32.builder().setEncodeTable(encodeTable).setLineLength(0).get();
408+
final byte[] data = { 0 };
409+
final byte[] encoded = base32.encode(data);
410+
assertEquals("BB======", new String(encoded, StandardCharsets.US_ASCII));
411+
assertArrayEquals(data, base32.decode(encoded));
412+
}
413+
414+
@Test
415+
void testBuilderCustomEncodeTableRejectsDuplicateEntries() {
416+
final byte[] encodeTable = ENCODE_TABLE.clone();
417+
encodeTable[1] = encodeTable[0];
418+
assertThrows(IllegalArgumentException.class, () -> Base32.builder().setEncodeTable(encodeTable));
419+
}
420+
421+
@Test
422+
void testBuilderCustomEncodeTableRejectsInvalidLength() {
423+
assertThrows(IllegalArgumentException.class, () -> Base32.builder().setEncodeTable(Arrays.copyOf(ENCODE_TABLE, ENCODE_TABLE.length - 1)));
424+
}
425+
426+
@Test
427+
void testBuilderCustomEncodeTableWithNonAsciiBytes() {
428+
final byte[] encodeTable = new byte[32];
429+
for (int i = 0; i < encodeTable.length; i++) {
430+
encodeTable[i] = (byte) (0x80 + i);
431+
}
432+
final Base32 base32 = Base32.builder().setEncodeTable(encodeTable).setLineLength(0).get();
433+
final byte[] data = { 0 };
434+
final byte[] encoded = base32.encode(data);
435+
assertArrayEquals(new byte[] { (byte) 0x80, (byte) 0x80, '=', '=', '=', '=', '=', '=' }, encoded);
436+
assertTrue(base32.isInAlphabet((byte) 0x80));
437+
assertArrayEquals(data, base32.decode(encoded));
438+
}
439+
401440
@Test
402441
void testBuilderLineAttributes() {
403442
assertNull(Base32.builder().get().getLineSeparator());

0 commit comments

Comments
 (0)