/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion 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. */ package illarion.common.util; import javax.annotation.Nonnull; import java.io.*; import java.nio.charset.Charset; /** * Class to handle base64 strings. This class allows decoding and encoding such * strings. Base64 can be used easily to store binary values as a string in text * files. * * @author Martin Karing <nitram@illarion.org> */ @SuppressWarnings("SpellCheckingInspection") public final class Base64 { /** * Table of the sixty-four characters that are used as the Base64 alphabet: [a-z0-9A-Z+/] */ @Nonnull private static final byte[] base64Chars = {'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', '+', '/',}; /** * Reverse lookup table for the Base64 alphabet. */ @Nonnull private static final byte[] reverseBase64Chars = new byte[0x100]; /** * Symbol that represents the end of an input stream */ private static final int END_OF_INPUT = -1; /** * A character that is not a valid base 64 character. */ private static final int NON_BASE_64 = -1; /** * A character that is not a valid base 64 character. */ private static final int NON_BASE_64_PADDING = -3; /** * A character that is not a valid base 64 character. */ private static final int NON_BASE_64_WHITESPACE = -2; static { // Fill in NON_BASE_64 for all characters to start with for (int i = 0; i < reverseBase64Chars.length; i++) { reverseBase64Chars[i] = NON_BASE_64; } // For characters that are base64Chars, adjust // the reverse lookup table. for (byte i = 0; i < base64Chars.length; i++) { reverseBase64Chars[base64Chars[i]] = i; } reverseBase64Chars[' '] = NON_BASE_64_WHITESPACE; reverseBase64Chars['\n'] = NON_BASE_64_WHITESPACE; reverseBase64Chars['\r'] = NON_BASE_64_WHITESPACE; reverseBase64Chars['\t'] = NON_BASE_64_WHITESPACE; reverseBase64Chars['\f'] = NON_BASE_64_WHITESPACE; reverseBase64Chars['='] = NON_BASE_64_PADDING; } /** * This class need not be instantiated, all methods are static. */ private Base64() { // should not be called } /** * Decode Base64 encoded bytes. Characters that are not part of the Base64 * alphabet are ignored in the input. * * @param bytes The data to decode. * @return Decoded bytes. */ @Nonnull public static byte[] decode(@Nonnull byte[] bytes) { ByteArrayInputStream in = new ByteArrayInputStream(bytes); // calculate the length of the resulting output. // in general it will be at most 3/4 the size of the input // but the input length must be divisible by four. // If it isn't the next largest size that is divisible // by four is used. int mod; int length = bytes.length; if ((mod = length % 4) != 0) { length += 4 - mod; } length = (length * 3) / 4; ByteArrayOutputStream out = new ByteArrayOutputStream(length); try { decode(in, out, false); } catch (@Nonnull IOException x) { // This can't happen. // The input and output streams were constructed // on memory structures that don't actually use IO. throw new IllegalStateException(x); } return out.toByteArray(); } /** * Decode Base64 encoded bytes to the an OutputStream. Characters that are * not part of the Base64 alphabet are ignored in the input. * * @param bytes The data to decode. * @param out Stream to which to write decoded data. * @throws IOException if an IO error occurs. */ public static void decode(@Nonnull byte[] bytes, @Nonnull OutputStream out) throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(bytes); decode(in, out, false); } /** * Decode Base64 encoded data from the InputStream to the OutputStream. * Characters in the Base64 alphabet, white space and equals sign are * expected to be in url encoded data. The presence of other characters * could be a sign that the data is corrupted. * * @param in Stream from which to read data that needs to be decoded. * @param out Stream to which to write decoded data. * @throws IOException if an IO error occurs. */ public static void decode(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException { decode(in, out, true); } /** * Decode Base64 encoded data from the InputStream to the OutputStream. * Characters in the Base64 alphabet, white space and equals sign are * expected to be in url encoded data. The presence of other characters * could be a sign that the data is corrupted. * * @param in Stream from which to read data that needs to be decoded. * @param out Stream to which to write decoded data. * @param throwExceptions Whether to throw exceptions when unexpected data * is encountered. * @throws IOException if an IO error occurs. */ public static void decode( @Nonnull InputStream in, @Nonnull OutputStream out, boolean throwExceptions) throws IOException { // Base64 decoding converts four bytes of input to three bytes of output int[] inBuffer = new int[4]; // read bytes unmapping them from their ASCII encoding in the process // we must read at least two bytes to be able to output anything boolean done = false; while (!done && ((inBuffer[0] = readBase64(in, throwExceptions)) != END_OF_INPUT) && ((inBuffer[1] = readBase64(in, throwExceptions)) != END_OF_INPUT)) { // Fill the buffer inBuffer[2] = readBase64(in, throwExceptions); inBuffer[3] = readBase64(in, throwExceptions); // Calculate the output // The first two bytes of our in buffer will always be valid // but we must check to make sure the other two bytes // are not END_OF_INPUT before using them. // The basic idea is that the four bytes will get reconstituted // into three bytes along these lines: // [xxAAAAAA] [xxBBBBBB] [xxCCCCCC] [xxDDDDDD] // [AAAAAABB] [BBBBCCCC] [CCDDDDDD] // bytes are considered to be zero when absent. // six A and two B out.write((inBuffer[0] << 2) | (inBuffer[1] >> 4)); if (inBuffer[2] == END_OF_INPUT) { done = true; } else { // four B and four C out.write((inBuffer[1] << 4) | (inBuffer[2] >> 2)); if (inBuffer[3] == END_OF_INPUT) { done = true; } else { // two C and six D out.write((inBuffer[2] << 6) | inBuffer[3]); } } } out.flush(); } /** * Decode a Base64 encoded String. Characters that are not part of the * Base64 alphabet are ignored in the input. The String is converted to and * from bytes according to the platform's default character encoding. * * @param string The data to decode. * @return A decoded String. */ @Nonnull public static String decode(@Nonnull String string) { Charset charset = Charset.defaultCharset(); return new String(decode(string.getBytes(charset)), charset); } /** * Decode a Base64 encoded String. Characters that are not part of the * Base64 alphabet are ignored in the input. * * @param string The data to decode. * @param enc Character encoding to use when converting to and from bytes. * @return A decoded String. * @throws UnsupportedEncodingException if the character encoding specified * is not supported. */ @Nonnull public static String decode(@Nonnull String string, @Nonnull String enc) throws UnsupportedEncodingException { return new String(decode(string.getBytes(enc)), enc); } /** * Decode a Base64 encoded String. Characters that are not part of the * Base64 alphabet are ignored in the input. * * @param string The data to decode. * @param encIn Character encoding to use when converting input to bytes * (should not matter because Base64 data is designed to survive * most character encodings) * @param encOut Character encoding to use when converting decoded bytes to * output. * @return A decoded String. * @throws UnsupportedEncodingException if the character encoding specified * is not supported. */ @Nonnull public static String decode(@Nonnull String string, @Nonnull String encIn, @Nonnull String encOut) throws UnsupportedEncodingException { return new String(decode(string.getBytes(encIn)), encOut); } /** * Encode bytes in Base64. No line breaks or other white space are inserted * into the encoded data. * * @param bytes The data to encode. * @return Encoded bytes. */ @Nonnull public static byte[] encode(@Nonnull byte[] bytes) { return encode(bytes, false); } /** * Encode bytes in Base64. * * @param bytes The data to encode. * @param lineBreaks Whether to insert line breaks every 76 characters in * the output. * @return Encoded bytes. */ @Nonnull public static byte[] encode(@Nonnull byte[] bytes, boolean lineBreaks) { ByteArrayInputStream in = new ByteArrayInputStream(bytes); // calculate the length of the resulting output. // in general it will be 4/3 the size of the input // but the input length must be divisible by three. // If it isn't the next largest size that is divisible // by three is used. int mod; int length = bytes.length; if ((mod = length % 3) != 0) { length += 3 - mod; } length = (length * 4) / 3; ByteArrayOutputStream out = new ByteArrayOutputStream(length); try { encode(in, out, lineBreaks); } catch (@Nonnull IOException x) { // This can't happen. // The input and output streams were constructed // on memory structures that don't actually use IO. throw new IllegalStateException(x); } return out.toByteArray(); } /** * Encode data from the InputStream to the OutputStream in Base64. Line * breaks are inserted every 76 characters in the output. * * @param in Stream from which to read data that needs to be encoded. * @param out Stream to which to write encoded data. * @throws IOException if there is a problem reading or writing. */ public static void encode(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException { encode(in, out, true); } /** * Encode data from the InputStream to the OutputStream in Base64. * * @param in Stream from which to read data that needs to be encoded. * @param out Stream to which to write encoded data. * @param lineBreaks Whether to insert line breaks every 76 characters in * the output. * @throws IOException if there is a problem reading or writing. */ public static void encode( @Nonnull InputStream in, @Nonnull OutputStream out, boolean lineBreaks) throws IOException { // Base64 encoding converts three bytes of input to // four bytes of output int[] inBuffer = new int[3]; int lineCount = 0; boolean done = false; while (!done && ((inBuffer[0] = in.read()) != END_OF_INPUT)) { // Fill the buffer inBuffer[1] = in.read(); inBuffer[2] = in.read(); // Calculate the out Buffer // The first byte of our in buffer will always be valid // but we must check to make sure the other two bytes // are not END_OF_INPUT before using them. // The basic idea is that the three bytes get split into // four bytes along these lines: // [AAAAAABB] [BBBBCCCC] [CCDDDDDD] // [xxAAAAAA] [xxBBBBBB] [xxCCCCCC] [xxDDDDDD] // bytes are considered to be zero when absent. // the four bytes are then mapped to common ASCII symbols // A's: first six bits of first byte out.write(base64Chars[inBuffer[0] >> 2]); if (inBuffer[1] == END_OF_INPUT) { // B's: last two bits of first byte out.write(base64Chars[(inBuffer[0] << 4) & 0x30]); // an equal signs for characters that is not a Base64 characters out.write('='); out.write('='); done = true; } else { // B's: last two bits of first byte, first four bits of second // byte out.write(base64Chars[((inBuffer[0] << 4) & 0x30) | (inBuffer[1] >> 4)]); if (inBuffer[2] == END_OF_INPUT) { // C's: last four bits of second byte out.write(base64Chars[(inBuffer[1] << 2) & 0x3c]); // an equals sign for a character that is not a Base64 // character out.write('='); done = true; } else { // C's: last four bits of second byte, first two bits of // third byte out.write(base64Chars[((inBuffer[1] << 2) & 0x3c) | (inBuffer[2] >> 6)]); // D's: last six bits of third byte out.write(base64Chars[inBuffer[2] & 0x3F]); } } lineCount += 4; if (lineBreaks && (lineCount >= 76)) { out.write('\n'); lineCount = 0; } } if (lineBreaks && (lineCount >= 1)) { out.write('\n'); } out.flush(); } /** * Encode a String in Base64. The String is converted to and from bytes * according to the platform's default character encoding. No line breaks or * other white space are inserted into the encoded data. * * @param string The data to encode. * @return An encoded String. */ @Nonnull public static String encode(@Nonnull String string) { Charset charset = Charset.defaultCharset(); return new String(encode(string.getBytes(charset)), charset); } /** * Encode a String in Base64. No line breaks or other white space are * inserted into the encoded data. * * @param string The data to encode. * @param enc Character encoding to use when converting to and from bytes. * @return An encoded String. * @throws UnsupportedEncodingException if the character encoding specified * is not supported. */ @Nonnull public static String encode(@Nonnull String string, @Nonnull String enc) throws UnsupportedEncodingException { return new String(encode(string.getBytes(enc)), enc); } /** * Reads the next (decoded) Base64 character from the input stream. Non * Base64 characters are skipped. * * @param in Stream from which bytes are read. * @param throwExceptions Throw an exception if an unexpected character is * encountered. * @return the next Base64 character from the stream or -1 if there are no * more Base64 characters on the stream. * @throws IOException if an IO Error occurs. * @throws IllegalStateException if unexpected data is encountered when * throwExceptions is specified. * @since ostermillerutils 1.00.00 */ private static int readBase64( @Nonnull InputStream in, boolean throwExceptions) throws IOException { int read; int numPadding = 0; do { read = in.read(); if (read == END_OF_INPUT) { return END_OF_INPUT; } read = reverseBase64Chars[(byte) read]; if (throwExceptions && ((read == NON_BASE_64) || ((numPadding > 0) && (read > NON_BASE_64)))) { throw new IllegalStateException("Unexpected character"); //$NON-NLS-1$ } if (read == NON_BASE_64_PADDING) { numPadding++; } } while (read <= NON_BASE_64); return read; } }