diff --git a/src/main/java/org/apache/commons/codec/binary/Base64.java b/src/main/java/org/apache/commons/codec/binary/Base64.java index f515b510fe..c67c5bc5c0 100644 --- a/src/main/java/org/apache/commons/codec/binary/Base64.java +++ b/src/main/java/org/apache/commons/codec/binary/Base64.java @@ -111,7 +111,7 @@ public class Base64 extends BaseNCodec { * Thanks to "commons" project in ws.apache.org for this code. * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ */ - private static final byte[] DECODE_TABLE = { + private static final byte[] DEFAULT_DECODE_TABLE = { // 0 1 2 3 4 5 6 7 8 9 A B C D E F -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f @@ -134,14 +134,18 @@ public class Base64 extends BaseNCodec { // some state be preserved between calls of encode() and decode(). /** - * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able - * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch - * between the two modes. + * Encode table to use: either STANDARD or URL_SAFE or custom. + * Note: the DEFAULT_DECODE_TABLE above remains static for STANDARD and URL_SAFA + * because it is able to decode both STANDARD and URL_SAFE streams, + * but the encodeTable must be a member variable so we can switch + * between modes. */ private final byte[] encodeTable; - // Only one decode table currently; keep for consistency with Base32 code - private final byte[] decodeTable = DECODE_TABLE; + /** + * Decode table to use + */ + private final byte[] decodeTable; /** * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. @@ -271,9 +275,47 @@ public Base64(final int lineLength, final byte[] lineSeparator) { * @since 1.4 */ public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { + this(lineLength, lineSeparator, urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE); + } + + /** + * Creates a Base64 codec used for decoding and encoding with non-standard encodeTable-table + * + * @param encodeTable + * The manual encodeTable - a byte array of 64 chars + */ + public Base64(byte[] encodeTable) { + this(0, CHUNK_SEPARATOR, encodeTable); + } + + /** + * Creates a Base64 codec used for decoding and encoding with non-standard encode-table and given lineLength and lineSeparator + * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param encodeTable + * The manual encodeTable - a byte array of 64 chars + */ + public Base64(final int lineLength, final byte[] lineSeparator, byte[] encodeTable) { super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength, lineSeparator == null ? 0 : lineSeparator.length); + + this.encodeTable = encodeTable; + + if (encodeTable == STANDARD_ENCODE_TABLE || encodeTable == URL_SAFE_ENCODE_TABLE) { + decodeTable = DEFAULT_DECODE_TABLE; + } else { + if (encodeTable.length != 64) { + throw new IllegalArgumentException("encodeTable must be exactly 64 bytes long"); + } + decodeTable = calculateDecodeTable(encodeTable); + } + // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0 // @see test case Base64Test.testConstructors() if (lineSeparator != null) { @@ -294,7 +336,23 @@ public Base64(final int lineLength, final byte[] lineSeparator, final boolean ur this.lineSeparator = null; } this.decodeSize = this.encodeSize - 1; - this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + } + + /** + * calculates a decode table for a given encode table + * + * @param encodeTable + * @return decodeTable + */ + private byte[] calculateDecodeTable(byte[] encodeTable) { + byte[] decodeTable = new byte[256]; + for (int i=0; i < 256; i++) { + decodeTable[i] = -1; + } + for (int i=0; i < encodeTable.length; i++) { + decodeTable[(int) encodeTable[i]] = (byte) i; + } + return decodeTable; } /** @@ -313,7 +371,7 @@ public boolean isUrlSafe() { * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last * remaining bytes (if not multiple of 3). *

- *

Note: no padding is added when encoding using the URL-safe alphabet.

+ *

Note: no padding is added when encoding not using the default alphabet.

*

* Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ @@ -441,8 +499,8 @@ void decode(final byte[] in, int inPos, final int inAvail, final Context context context.eof = true; break; } - if (b >= 0 && b < DECODE_TABLE.length) { - final int result = DECODE_TABLE[b]; + if (b >= 0 && b < decodeTable.length) { + final int result = decodeTable[b]; if (result >= 0) { context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK; context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; @@ -509,7 +567,7 @@ public static boolean isArrayByteBase64(final byte[] arrayOctet) { * @since 1.4 */ public static boolean isBase64(final byte octet) { - return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + return octet == PAD_DEFAULT || (octet >= 0 && octet < DEFAULT_DECODE_TABLE.length && DEFAULT_DECODE_TABLE[octet] != -1); } /** diff --git a/src/test/java/org/apache/commons/codec/binary/Base64Test.java b/src/test/java/org/apache/commons/codec/binary/Base64Test.java index a9b98673df..7168409c16 100644 --- a/src/test/java/org/apache/commons/codec/binary/Base64Test.java +++ b/src/test/java/org/apache/commons/codec/binary/Base64Test.java @@ -32,6 +32,7 @@ import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.EncoderException; import org.apache.commons.lang3.ArrayUtils; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -115,6 +116,56 @@ public void testBase64() { assertEquals("decode hello world", "Hello World", decodeString); } + @Test(expected = IllegalArgumentException.class) + public void testCustomEncodingAlphabet_illegal() { + byte[] encodeTable = { + '.', '-', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M' + }; + Base64 b64customEncoding = new Base64(encodeTable); + } + + @Test + public void testCustomEncodingAlphabet() { + // created a duplicate of STANDARD_ENCODE_TABLE and replaced two chars with + // custom values not already present in table + // A => . B => - + byte[] encodeTable = { + '.', '-', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + // two instances: one with default table and one with adjusted encoding table + Base64 b64 = new Base64(); + Base64 b64customEncoding = new Base64(encodeTable); + + final String content = "! Hello World - this ยง$%"; + + byte[] encodedBytes = b64.encode(StringUtils.getBytesUtf8(content)); + String encodedContent = StringUtils.newStringUtf8(encodedBytes); + + byte[] encodedBytesCustom = b64customEncoding.encode(StringUtils.getBytesUtf8(content)); + String encodedContentCustom = StringUtils.newStringUtf8(encodedBytesCustom); + + Assert.assertTrue("testing precondition not met - ecodedContent should contain parts of modified table", + encodedContent.contains("A") && encodedContent.contains("B")); + + Assert.assertEquals("custom encoding mismatch to expected - " + encodedContentCustom, + encodedContent + .replaceAll("A", ".").replaceAll("B", "-") // replace alphabet adjustments + .replaceAll("=", "") // remove padding (not default alphabet) + , encodedContentCustom); + + + // try decode encoded content + final byte[] decode = b64customEncoding.decode(encodedBytesCustom); + final String decodeString = StringUtils.newStringUtf8(decode); + + Assert.assertEquals(content, decodeString); + } + @Test public void testBase64AtBufferStart() { testBase64InBuffer(0, 100);