/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io.base64;
import java.io.IOException;
import java.io.OutputStream;
/**
* This <code>OuputStream</code> encodes supplied data to Base64 encoding and writes it to the underlying
* <code>OutputStream</code>.
*
* @see Base64Encoder
* @author Maxence Bernard, with the exception of the algorithm description which was found on the Web.
*/
public class Base64OutputStream extends OutputStream {
/*
Base64 uses a 65 character subset of US-ASCII,
allowing 6 bits for each character so the character
"m" with a Base64 value of 38, when represented
in binary form, is 100110.
With a text string, let's say "men" is encoded this
is what happens :
The text string is converted into its US-ASCII value.
The character "m" has the decimal value of 109
The character "e" has the decimal value of 101
The character "n" has the decimal value of 110
When converted to binary the string looks like this :
m 01101101
e 01100101
n 01101110
These three "8-bits" are concatenated to make a
24 bit stream
011011010110010101101110
This 24 bit stream is then split up into 4 6-bit
sections
011011 010110 010101 101110
We now have 4 values. These binary values are
converted to decimal form
27 22 21 46
And the corresponding Base64 character are :
b W V u
The encoding is always on a three characters basis
(to have a set of 4 Base64 characters). To encode one
or two then, we use the special character "=" to pad
until 4 base64 characters is reached.
ex. encode "me"
01101101 01100101
0110110101100101
011011 010110 0101
111111 (AND to fill the missing bits)
011011 010110 010100
b W U
b W U = ("=" is the padding character)
so "bWU=" is the base64 equivalent.
encode "m"
01101101
011011 01
111111 (AND to fill the missing bits)
011011 010000
b Q = = (two paddings are added)
Finally, MIME specifies that lines are 76 characters wide maximum.
*/
/** Underlying OutputStream encoded data is sent to */
private OutputStream out;
/** The Base64 encoding table */
private final byte[] encodingTable;
/** The character used for padding */
private final byte paddingChar;
/** Array used to accumulate the first 2 bytes of a 3-byte group */
private byte byteAcc[] = new byte[2];
/** Number of bytes accumulated to form a 3-byte group */
private int nbBytesWaiting;
/** Specifies whether line breaks should be inserted after 80 chars */
private boolean insertLineBreaks;
/** Current line length (to insert line return character after 80 chars)*/
private int lineLength;
/**
* Equivalent to calling {@link #Base64OutputStream(java.io.OutputStream, boolean, Base64Table)} with
* a {@link Base64Table#STANDARD_TABLE} table.
*
* @param out the underlying OutputStream to write the base64-encoded data to
* @param insertLineBreaks if <code>true</code>, line breaks will be inserted after every 80 characters written
*/
public Base64OutputStream(OutputStream out, boolean insertLineBreaks) {
this(out, insertLineBreaks, Base64Table.STANDARD_TABLE);
}
/**
* Creates a new <code>Base64OutputStream</code> using the underlying OutputStream and table to write the
* base64-encoded data.
*
* @param out the underlying OutputStream to write the base64-encoded data to
* @param insertLineBreaks if <code>true</code>, line breaks will be inserted after every 80 characters written
* @param table the table to use to encode data
*/
public Base64OutputStream(OutputStream out, boolean insertLineBreaks, Base64Table table) {
this.out = out;
this.insertLineBreaks = insertLineBreaks;
this.encodingTable = table.getEncodingTable();
this.paddingChar = table.getPaddingChar();
}
/**
* Writes padding '=' characters to the underlying <code>OutputStream</code> if there currently is an
* unfinished 3-byte group. If it's not the case, then this method is a no-op.
*
* @throws IOException if the padding characters could not be written to the underlying OutputStream.
*/
public void writePadding() throws IOException {
// No padding needed
if(nbBytesWaiting==0)
return;
// 1 padding character
if (nbBytesWaiting==2) {
// 2 bytes left
out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
out.write(encodingTable[(byte)((byteAcc[1] & 0x0F) << 2)]);
out.write(paddingChar);
}
// 2 padding characters
else if (nbBytesWaiting==1) {
// 1 byte left
out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
out.write(encodingTable[(byte)((byteAcc[0] & 0x03) << 4)]);
out.write(paddingChar);
out.write(paddingChar);
}
// Just in case this method is called again
nbBytesWaiting = 0;
}
/////////////////////////////////
// OutputStream implementation //
/////////////////////////////////
@Override
public void write(int i) throws IOException {
// We have a 3-byte group
if(nbBytesWaiting==2) {
// Write 3 bytes as 4 base64 characters
out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]);
out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]);
out.write(encodingTable[(byte)(((byteAcc[1] & 0x0F) << 2) | ((i & 0xC0) >> 6))]);
out.write(encodingTable[(byte)(i & 0x3F)]);
nbBytesWaiting = 0;
// Insert a line break after every 80 characters written
if (insertLineBreaks && (lineLength += 4) >= 76) {
out.write('\r');
out.write('\n');
lineLength = 0;
}
}
// Waiting for more bytes...
else {
byteAcc[nbBytesWaiting++] = (byte)i;
}
}
/**
* Writes padding if necessary and closes the underlying stream.
*/
@Override
public void close() throws IOException {
writePadding();
out.close();
}
}