Skip to content
Closed
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
80 changes: 69 additions & 11 deletions src/main/java/org/apache/commons/codec/binary/Base64.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

/**
Expand All @@ -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).
* </p>
* <p><b>Note: no padding is added when encoding using the URL-safe alphabet.</b></p>
* <p><b>Note: no padding is added when encoding not using the default alphabet.</b></p>
* <p>
* 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/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/org/apache/commons/codec/binary/Base64Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down