Base64Decoder.java
package org.djutils.decoderdumper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.djutils.logger.CategoryLogger;
/**
* Decode base64 encoded data and show it as hex bytes. See https://en.wikipedia.org/wiki/Base64
* <p>
* Copyright (c) 2013-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
* <p>
* @version $Revision$, $LastChangedDate$, by $Author$, initial version Jan 7, 2019 <br>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
*/
public class Base64Decoder implements Decoder
{
/** Assembling space for decoded data. */
private int notYetDecodedData = 0;
/** Number of accumulated bits in <code>notYetDecodedData</code>. */
private int accumulatedBits = 0;
/** Dumper used internally to assemble the decoded data into hex values and char values. */
private final Dumper<Base64Decoder> internalDumper = new Dumper<>();
/** Collector for the output of the internal dumper. */
private final ByteArrayOutputStream baos;
/** Count number of equals (=) symbols seen. */
private int endOfInputCharsSeen = 0;
/** Set when an error is detected in the input stream. */
private boolean errorDetected = false;
/**
* Construct a new Base64Decoder.
* @param decodedBytesPerLine int; maximum number of decoded input characters resulting in one output line
* @param extraSpaceAfterEvery int; insert an extra space after every N output fields (a multiple of 3 makes sense for the
* base64 decoder because base64 encodes three bytes into 4 characters)
*/
public Base64Decoder(final int decodedBytesPerLine, final int extraSpaceAfterEvery)
{
this.baos = new ByteArrayOutputStream();
this.internalDumper.setOutputStream(this.baos);
int maximumBytesPerOutputLine = (decodedBytesPerLine + 3) / 4 * 3;
this.internalDumper.addDecoder(new HexDecoder(maximumBytesPerOutputLine, extraSpaceAfterEvery));
this.internalDumper.addDecoder(new FixedString(" "));
this.internalDumper.addDecoder(new CharDecoder(maximumBytesPerOutputLine, extraSpaceAfterEvery));
}
/** {@inheritDoc} */
@Override
public String getResult()
{
try
{
this.internalDumper.flush();
String result = this.baos.toString("UTF-8");
this.baos.reset();
return result;
}
catch (IOException ioe)
{
// Cannot happen because writing to a ByteArrayOutputStream should never fail
return null;
}
}
/** {@inheritDoc} */
@Override
public int getMaximumWidth()
{
return this.internalDumper.getMaximumWidth();
}
/** {@inheritDoc} */
@Override
public boolean append(final int address, final byte theByte) throws IOException
{
if (theByte == 61)
{
this.endOfInputCharsSeen++; // equals
}
if (this.endOfInputCharsSeen > 0)
{
return false; // This decoder does not handle multiple base64 encoded objects in its input
}
int value;
if (theByte >= 48 && theByte <= 57)
{
value = theByte - 48 + 52; // Digit
}
else if (theByte >= 65 && theByte <= 90)
{
value = theByte - 65 + 0; // Capital letter
}
else if (theByte >= 97 && theByte <= 122)
{
value = theByte - 97 + 26;
}
else if (theByte == 43 || theByte == 45 || theByte == 46)
{
value = 62; // Plus or minus or dot
}
else if (theByte == 47 || theByte == 95 || theByte == 44)
{
value = 63; // Slash or underscore or comma
}
else if (theByte <= 32)
{
return false; // White space can appear anywhere and should be ignored (but this test for white space is bad)
}
else
{
// Illegal byte in input
if (!this.errorDetected) // First error
{
CategoryLogger.always().info("illegal character found in Base64Decoder stream at address {}, character {}",
address, theByte);
}
this.errorDetected = true;
return false;
}
this.notYetDecodedData = (this.notYetDecodedData << 6) + value;
this.accumulatedBits += 6;
if (this.accumulatedBits >= 8)
{
int byteValue = this.notYetDecodedData >> (this.accumulatedBits - 8);
this.accumulatedBits -= 8;
this.notYetDecodedData -= byteValue << this.accumulatedBits;
boolean result = this.internalDumper.append((byte) byteValue);
return result;
}
return false;
}
/** {@inheritDoc} */
@Override
public boolean ignoreForIdenticalOutputCheck()
{
return false;
}
}