Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
63 changes: 58 additions & 5 deletions src/main/java/org/apache/commons/codec/binary/Base32.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,19 @@ public Base32 get() {
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);
}

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

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
Expand Down Expand Up @@ -256,6 +268,29 @@ public static Builder builder() {
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;
}
Expand All @@ -276,6 +311,23 @@ private static byte[] encodeTable(final boolean useHex) {
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;}
Expand Down Expand Up @@ -530,14 +582,14 @@ void decode(final byte[] input, int inPos, final int inAvail, final Context cont
}
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;
Expand Down Expand Up @@ -738,7 +790,8 @@ byte[] getLineSeparator() {
*/
@Override
public boolean isInAlphabet(final byte octet) {
return isInAlphabet(octet, decodeTable);
final int value = octet & 0xff;
return value < decodeTable.length && decodeTable[value] != -1;
}

/**
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/org/apache/commons/codec/binary/Base32Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,45 @@ void testBuilderCodecPolicy() {
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());
Expand Down
Loading