This class provides encoding of byte arrays into Base64-encoded strings, and decoding the other way.
package freenet.support; /** * This class provides encoding of byte arrays into Base64-encoded strings, * and decoding the other way. * * <P>NOTE! This is modified Base64 with slightly different characters than * usual, so it won't require escaping when used in URLs. * * <P>NOTE! This class only does the padding that's normal in Base64 * if the 'true' flag is given to the encode() method. This is because * Base64 requires that the length of the encoded text be a multiple * of four characters, padded with '='. Without the 'true' flag, we don't * add these '=' characters. * * @author Stephen Blackheath */ public class Base64 { private static char[] base64Alphabet = { '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', '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', '~', '-'}; private static char[] base64StandardAlphabet = { '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', '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', '+', '/'}; /** * A reverse lookup table to convert base64 letters back into the * a 6-bit sequence. */ private static byte[] base64Reverse; private static byte[] base64StandardReverse; // Populate the base64Reverse lookup table from the base64Alphabet table. static { base64Reverse = new byte[128]; base64StandardReverse = new byte[base64Reverse.length]; // Set all entries to 0xFF, which means that that particular letter // is not a legal base64 letter. for (int i = 0; i < base64Reverse.length; i++) { base64Reverse[i] = (byte) 0xFF; base64StandardReverse[i] = (byte) 0xFF; } for (int i = 0; i < base64Alphabet.length; i++) { base64Reverse[base64Alphabet[i]] = (byte) i; base64StandardReverse[base64StandardAlphabet[i]] = (byte) i; } } /** * Encode to our shortened (non-standards-compliant) format. */ public static String encode(byte[] in) { return encode(in, false); } /* FIXME: Figure out where this function is used and maybe remove it if its not * used. Its old javadoc which has been here for a while fools the user into believing * that the format is standard compliant */ /** * Caller should specify equalsPad=true if they want a standards compliant padding, * but not standard compliant encoding. */ public static String encode(byte[] in, boolean equalsPad) { return encode(in, equalsPad, base64Alphabet); } /** * Standard compliant encoding. */ public static String encodeStandard(byte[] in) { return encode(in, true, base64StandardAlphabet); } /** * Caller should specify equalsPad=true if they want a standards compliant encoding. */ private static String encode(byte[] in, boolean equalsPad, char[] alphabet) { char[] out = new char[((in.length+2)/3)*4]; int rem = in.length%3; int o = 0; for (int i = 0; i < in.length;) { int val = (in[i++] & 0xFF) << 16; if (i < in.length) val |= (in[i++] & 0xFF) << 8; if (i < in.length) val |= (in[i++] & 0xFF); out[o++] = alphabet[(val>>18) & 0x3F]; out[o++] = alphabet[(val>>12) & 0x3F]; out[o++] = alphabet[(val>>6) & 0x3F]; out[o++] = alphabet[val & 0x3F]; } int outLen = out.length; switch (rem) { case 1: outLen -= 2; break; case 2: outLen -= 1; break; } // Pad with '=' signs up to a multiple of four if requested. if (equalsPad) while (outLen < out.length) out[outLen++] = '='; return new String(out, 0, outLen); } /** * Handles the standards-compliant padding (padded with '=' signs) as well as our * shortened form. * @throws IllegalBase64Exception */ public static byte[] decode(String inStr) throws Exception { return decode(inStr, base64Reverse); } /** * Handles the standards-compliant base64 encoding. */ public static byte[] decodeStandard(String inStr) throws Exception { return decode(inStr, base64StandardReverse); } /** * Handles the standards-compliant (padded with '=' signs) as well as our * shortened form. */ private static byte[] decode(String inStr, byte[] reverseAlphabet) throws Exception { try { char[] in = inStr.toCharArray(); int inLength = in.length; // Strip trailing equals signs. while ((inLength > 0) && (in[inLength-1] == '=')) inLength--; int blocks = inLength/4; int remainder = inLength & 3; // wholeInLen and wholeOutLen are the the length of the input and output // sequences respectively, not including any partial block at the end. int wholeInLen = blocks*4; int wholeOutLen = blocks*3; int outLen = wholeOutLen; switch (remainder) { case 1: throw new Exception("illegal Base64 length"); case 2: outLen = wholeOutLen+1; break; case 3: outLen = wholeOutLen+2; break; default: outLen = wholeOutLen; } byte[] out = new byte[outLen]; int o = 0; int i; for (i = 0; i < wholeInLen;) { int in1 = reverseAlphabet[in[i]]; int in2 = reverseAlphabet[in[i+1]]; int in3 = reverseAlphabet[in[i+2]]; int in4 = reverseAlphabet[in[i+3]]; int orValue = in1|in2|in3|in4; if ((orValue & 0x80) != 0) throw new Exception("illegal Base64 character"); int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6) | in4; out[o] = (byte) (outVal>>16); out[o+1] = (byte) (outVal>>8); out[o+2] = (byte) outVal; i += 4; o += 3; } int orValue; switch (remainder) { case 2: { int in1 = reverseAlphabet[in[i]]; int in2 = reverseAlphabet[in[i+1]]; orValue = in1|in2; int outVal = (in1 << 18) | (in2 << 12); out[o] = (byte) (outVal>>16); } break; case 3: { int in1 = reverseAlphabet[in[i]]; int in2 = reverseAlphabet[in[i+1]]; int in3 = reverseAlphabet[in[i+2]]; orValue = in1|in2|in3; int outVal = (in1 << 18) | (in2 << 12) | (in3 << 6); out[o] = (byte) (outVal>>16); out[o+1] = (byte) (outVal>>8); } break; default: // Keep compiler happy orValue = 0; } if ((orValue & 0x80) != 0) throw new Exception("illegal Base64 character"); return out; } // Illegal characters can cause an ArrayIndexOutOfBoundsException when // looking up reverseAlphabet. catch (ArrayIndexOutOfBoundsException e) { throw new Exception("illegal Base64 character"); } } } /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package freenet.support; import java.util.Arrays; import java.util.Random; import junit.framework.TestCase; /** * Test case for {@link freenet.support.Base64} class. * * @author Alberto Bacchelli <sback@freenetproject.org> */ public class Base64Test extends TestCase { /** * Test the encode(byte[]) method * against a well-known example * (see http://en.wikipedia.org/wiki/Base_64 as reference) * to verify if it encode works correctly. */ public void testEncode() { String toEncode = "Man is distinguished, not only by his reason, but by this singular " + "passion from other animals, which is a lust of the mind, that by a perseverance " + "of delight in the continued and indefatigable generation of knowledge, exceeds " + "the short vehemence of any carnal pleasure."; String expectedResult = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ" + "1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIG" + "x1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY" + "29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRz" + "IHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4"; byte[] aByteArrayToEncode = toEncode.getBytes(); assertEquals(Base64.encode(aByteArrayToEncode),expectedResult); } /** * Test the decode(String) method * against a well-known example * (see http://en.wikipedia.org/wiki/Base_64 as reference) * to verify if it decode an already encoded string correctly. */ public void testDecode() { String toDecode = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ" + "1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIG" + "x1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY" + "29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRz" + "IHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="; String expectedResult = "Man is distinguished, not only by his reason, but by this singular " + "passion from other animals, which is a lust of the mind, that by a perseverance " + "of delight in the continued and indefatigable generation of knowledge, exceeds " + "the short vehemence of any carnal pleasure."; try { String decodedString = new String(Base64.decode(toDecode)); assertEquals(decodedString,expectedResult); } catch (IllegalBase64Exception aException) { fail("Not expected exception thrown : " + aException.getMessage()); } } /** * Test encode(byte[] in) * and decode(String inStr) methods, * to verify if they work correctly together. * It compares the string before encoding * and with the one after decoding. */ public void testEncodeDecode() { byte[] bytesDecoded; byte[] bytesToEncode = new byte[5]; //byte upper bound bytesToEncode[0] = 127; bytesToEncode[1] = 64; bytesToEncode[2] = 0; bytesToEncode[3] = -64; //byte lower bound bytesToEncode[4] = -128; String aBase64EncodedString = Base64.encode(bytesToEncode); try { bytesDecoded = Base64.decode(aBase64EncodedString); assertTrue(Arrays.equals(bytesToEncode,bytesDecoded)); } catch (IllegalBase64Exception aException) { fail("Not expected exception thrown : " + aException.getMessage()); } } /** * Test the encode(String,boolean) * method to verify if the padding * character '=' is correctly placed. */ public void testEncodePadding() { byte[][] methodBytesArray = { //three byte Array -> no padding char expected {4,4,4}, //two byte Array -> one padding char expected {4,4}, //one byte Array -> two padding-chars expected {4}}; String encoded; for (int i = 0; i<methodBytesArray.length; i++) { encoded = Base64.encode(methodBytesArray[i],true); if (i == 0) //no occurrences expected assertEquals(encoded.indexOf('='),-1); else assertEquals(encoded.indexOf('='),encoded.length()-i); } } /** * Test if the decode(String) method * raise correctly an exception when * providing a string with non-Base64 * characters. */ public void testIllegalBaseCharacter() { // TODO: check many other possibile cases! String illegalCharString = "abcd=fghilmn"; try { Base64.decode(illegalCharString); fail("Expected IllegalBase64Exception not thrown"); } catch (IllegalBase64Exception exception) { assertSame("illegal Base64 character",exception.getMessage()); } } /** * Test if the decode(String) method * raise correctly an exception when * providing a string with a * wrong Base64 length. * (as we can consider not-padded strings too, * the only wrong lengths are the ones * where -> number MOD 4 = 1). */ public void testIllegalBaseLength() { //most interesting case String illegalLengthString = "a"; try { Base64.decode(illegalLengthString); fail("Expected IllegalBase64Exception not thrown"); } catch (IllegalBase64Exception exception) { assertSame("illegal Base64 length",exception.getMessage()); } } /** * Random test * * @throws IllegalBase64Exception */ public void testRandom() throws IllegalBase64Exception { int iter; Random r = new Random(1234); for (iter = 0; iter < 1000; iter++) { byte[] b = new byte[r.nextInt(64)]; for (int i = 0; i < b.length; i++) b[i] = (byte) (r.nextInt(256)); String encoded = Base64.encode(b); byte[] decoded = Base64.decode(encoded); assertEquals("length mismatch", decoded.length, b.length); for (int i = 0; i < b.length; i++) assertEquals("data mismatch: index " + i + " of " + b.length + " should be 0x" + Integer.toHexString(b[i] & 0xFF) + " was 0x" + Integer.toHexString(decoded[i] & 0xFF), b[i], decoded[i]); } } }