Base64Variant.java

/* Jackson JSON-processor.
 *
 * Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi
 */
package com.fasterxml.jackson.core;

import java.util.Arrays;

import com.fasterxml.jackson.core.util.ByteArrayBuilder;

/**
 * Class used to define specific details of which
 * variant of Base64 encoding/decoding is to be used. Although there is
 * somewhat standard basic version (so-called "MIME Base64"), other variants
 * exists, see <a href="http://en.wikipedia.org/wiki/Base64">Base64 Wikipedia entry</a> for details.
 *
 * @author Tatu Saloranta
 */
public final class Base64Variant
    implements java.io.Serializable
{
    /**
     * Defines how the Base64Variant deals with Padding while reading
     *
     * @since 2.12
     */
    public enum PaddingReadBehaviour {
        /**
         * Padding is not allowed in Base64 content being read (finding something
         * that looks like padding at the end of content results in an exception)
         */
        PADDING_FORBIDDEN,

        /**
         * Padding is required in Base64 content being read
         * (missing padding for incomplete ending quartet results in an exception)
         */
        PADDING_REQUIRED,

        /**
         * Padding is allowed but not required in Base64 content being read: no
         * exception thrown based on existence or absence, as long as proper
         * padding characters are used.
         */
        PADDING_ALLOWED
        ;
    }

    private final static int INT_SPACE = 0x20;

    // We'll only serialize name
    private static final long serialVersionUID = 1L;

    /**
     * Placeholder used by "no padding" variant, to be used when a character
     * value is needed.
     */
    protected final static char PADDING_CHAR_NONE = '\0';

    /**
     * Marker used to denote ascii characters that do not correspond
     * to a 6-bit value (in this variant), and is not used as a padding
     * character.
     */
    public final static int BASE64_VALUE_INVALID = -1;

    /**
     * Marker used to denote ascii character (in decoding table) that
     * is the padding character using this variant (if any).
     */
    public final static int BASE64_VALUE_PADDING = -2;

    /*
    /**********************************************************
    /* Encoding/decoding tables
    /**********************************************************
     */

    /**
     * Decoding table used for base 64 decoding.
     */
    private final transient int[] _asciiToBase64 = new int[128];

    /**
     * Encoding table used for base 64 decoding when output is done
     * as characters.
     */
    private final transient char[] _base64ToAsciiC = new char[64];

    /**
     * Alternative encoding table used for base 64 decoding when output is done
     * as ascii bytes.
     */
    private final transient byte[] _base64ToAsciiB = new byte[64];

    /*
    /**********************************************************
    /* Other configuration
    /**********************************************************
     */

    /**
     * Symbolic name of variant; used for diagnostics/debugging.
     *<p>
     * Note that this is the only non-transient field; used when reading
     * back from serialized state.
     *<p>
     * Also: must not be private, accessed from `BaseVariants`
     */
    final String _name;

    /**
     * Character used for padding, if any ({@link #PADDING_CHAR_NONE} if not).
     */
    private final char _paddingChar;

    /**
     * Maximum number of encoded base64 characters to output during encoding
     * before adding a linefeed, if line length is to be limited
     * ({@link java.lang.Integer#MAX_VALUE} if not limited).
     *<p>
     * Note: for some output modes (when writing attributes) linefeeds may
     * need to be avoided, and this value ignored.
     */
    private final int _maxLineLength;

    /**
     * Whether this variant uses padding when writing out content or not.
     *
     * @since 2.12
     */
    private final boolean _writePadding;

    /**
     * Whether padding characters should be required or not while decoding
     *
     * @since 2.12
     */
    private final PaddingReadBehaviour _paddingReadBehaviour;

    /*
    /**********************************************************
    /* Life-cycle
    /**********************************************************
     */

    public Base64Variant(String name, String base64Alphabet, boolean writePadding, char paddingChar, int maxLineLength)
    {
        _name = name;
        _writePadding = writePadding;
        _paddingChar = paddingChar;
        _maxLineLength = maxLineLength;

        // Ok and then we need to create codec tables.

        // First the main encoding table:
        int alphaLen = base64Alphabet.length();
        if (alphaLen != 64) {
            throw new IllegalArgumentException("Base64Alphabet length must be exactly 64 (was "+alphaLen+")");
        }

        // And then secondary encoding table and decoding table:
        base64Alphabet.getChars(0, alphaLen, _base64ToAsciiC, 0);
        Arrays.fill(_asciiToBase64, BASE64_VALUE_INVALID);
        for (int i = 0; i < alphaLen; ++i) {
            char alpha = _base64ToAsciiC[i];
            _base64ToAsciiB[i] = (byte) alpha;
            _asciiToBase64[alpha] = i;
        }

        // Plus if we use padding, add that in too
        if (writePadding) {
            _asciiToBase64[(int) paddingChar] = BASE64_VALUE_PADDING;
        }

        // By default, require padding on input if written on output; do not
        // accept if padding not written
        _paddingReadBehaviour = writePadding
                ? PaddingReadBehaviour.PADDING_REQUIRED
                : PaddingReadBehaviour.PADDING_FORBIDDEN;
    }

    /**
     * "Copy constructor" that can be used when the base alphabet is identical
     * to one used by another variant except for the maximum line length
     * (and obviously, name).
     *
     * @param base Variant to use for settings not specific by other parameters
     * @param name Name of this variant
     * @param maxLineLength Maximum length (in characters) of lines to output before
     *    using linefeed
     */
    public Base64Variant(Base64Variant base,
            String name, int maxLineLength)
    {
        this(base, name, base._writePadding, base._paddingChar, maxLineLength);
    }

    /**
     * "Copy constructor" that can be used when the base alphabet is identical
     * to one used by another variant, but other details (padding, maximum
     * line length) differ
     *
     * @param base Variant to use for settings not specific by other parameters
     * @param name Name of this variant
     * @param writePadding Whether variant will use padding when encoding
     * @param paddingChar Padding character used for encoding, excepted on reading, if any
     * @param maxLineLength Maximum length (in characters) of lines to output before
     *    using linefeed
     */
    public Base64Variant(Base64Variant base,
            String name, boolean writePadding, char paddingChar, int maxLineLength)
    {
        this(base, name, writePadding, paddingChar, base._paddingReadBehaviour, maxLineLength);
    }

    private Base64Variant(Base64Variant base,
            String name, boolean writePadding, char paddingChar, PaddingReadBehaviour paddingReadBehaviour, int maxLineLength)
    {
        _name = name;
        byte[] srcB = base._base64ToAsciiB;
        System.arraycopy(srcB, 0, this._base64ToAsciiB, 0, srcB.length);
        char[] srcC = base._base64ToAsciiC;
        System.arraycopy(srcC, 0, this._base64ToAsciiC, 0, srcC.length);
        int[] srcV = base._asciiToBase64;
        System.arraycopy(srcV, 0, this._asciiToBase64, 0, srcV.length);

        _writePadding = writePadding;
        _paddingChar = paddingChar;
        _maxLineLength = maxLineLength;
        _paddingReadBehaviour = paddingReadBehaviour;
    }

    private Base64Variant(Base64Variant base, PaddingReadBehaviour paddingReadBehaviour) {
        this(base, base._name, base._writePadding, base._paddingChar, paddingReadBehaviour, base._maxLineLength);
    }

    /**
     * @return Base64Variant which does not require padding on read
     *
     * @since 2.12
     */
    public Base64Variant withPaddingAllowed() {
        return withReadPadding(PaddingReadBehaviour.PADDING_ALLOWED);
    }

    /**
     * @return Base64Variant which requires padding on read
     * @since 2.12
     */
    public Base64Variant withPaddingRequired() {
        return withReadPadding(PaddingReadBehaviour.PADDING_REQUIRED);
    }

    /**
     * @return Base64Variant which does not accept padding on read
     * @since 2.12
     */
    public Base64Variant withPaddingForbidden() {
        return withReadPadding(PaddingReadBehaviour.PADDING_FORBIDDEN);
    }

    /**
     * @param readPadding Padding read behavior desired
     *
     * @return Instance with desired padding read behavior setting (this
     *   if already has setting; new instance otherwise)
     *
     * @since 2.12
     */
    public Base64Variant withReadPadding(PaddingReadBehaviour readPadding) {
        return (readPadding == _paddingReadBehaviour) ? this
                : new Base64Variant(this, readPadding);
    }

    /**
     * @param writePadding Determines if padding is output on write or not
     *
     * @return Base64Variant which writes padding or not depending on writePadding
     *
     * @since 2.12
     */
    public Base64Variant withWritePadding(boolean writePadding) {
        return (writePadding == _writePadding) ? this
                : new Base64Variant(this, _name, writePadding, _paddingChar, _maxLineLength);
    }

    /*
    /**********************************************************
    /* Serializable overrides
    /**********************************************************
     */

    // 26-Oct-2020, tatu: Much more complicated with 2.12 as it is
    //   possible to create differently configured instances.
    //   Need to start with name to regenerate tables etc but then
    //   handle overrides
    protected Object readResolve() {
        Base64Variant base = Base64Variants.valueOf(_name);
        if ((_writePadding != base._writePadding)
                || (_paddingChar != base._paddingChar)
                || (_paddingReadBehaviour != base._paddingReadBehaviour)
                || (_maxLineLength != base._maxLineLength)
                || (_writePadding != base._writePadding)
                ) {
            return new Base64Variant(base,
                    _name, _writePadding, _paddingChar, _paddingReadBehaviour, _maxLineLength);
        }
        return base;
    }

    /*
    /**********************************************************
    /* Public accessors
    /**********************************************************
     */

    public String getName() { return _name; }

    /**
     * @return True if this Base64 encoding will <b>write</b> padding on output
     *   (note: before Jackson 2.12 also dictated whether padding was accepted on read)
     */
    public boolean usesPadding() { return _writePadding; }

    /**
     * @return {@code True} if this variant requires padding on content decoded; {@code false} if not.
     *
     * @since 2.12
     */
    public boolean requiresPaddingOnRead() {
        return _paddingReadBehaviour == PaddingReadBehaviour.PADDING_REQUIRED;
    }

    /**
     * @return {@code True} if this variant accepts padding on content decoded; {@code false} if not.
     *
     * @since 2.12
     */
    public boolean acceptsPaddingOnRead() {
        return _paddingReadBehaviour != PaddingReadBehaviour.PADDING_FORBIDDEN;
    }

    public boolean usesPaddingChar(char c) { return c == _paddingChar; }
    public boolean usesPaddingChar(int ch) { return ch == (int) _paddingChar; }

    /**
     * @return Indicator on how this Base64 encoding will handle possible padding
     *   in content when reading.
     *
     * @since 2.12
     */
    public PaddingReadBehaviour paddingReadBehaviour() { return _paddingReadBehaviour; }

    public char getPaddingChar() { return _paddingChar; }
    public byte getPaddingByte() { return (byte)_paddingChar; }

    public int getMaxLineLength() { return _maxLineLength; }

    /*
    /**********************************************************
    /* Decoding support
    /**********************************************************
     */

    /**
     * @param c Character to decode
     *
     * @return 6-bit decoded value, if valid character;
     */
    public int decodeBase64Char(char c)
    {
        int ch = (int) c;
        return (ch <= 127) ? _asciiToBase64[ch] : BASE64_VALUE_INVALID;
    }

    public int decodeBase64Char(int ch)
    {
        return (ch <= 127) ? _asciiToBase64[ch] : BASE64_VALUE_INVALID;
    }

    public int decodeBase64Byte(byte b)
    {
        int ch = (int) b;
        // note: cast retains sign, so it's from -128 to +127
        if (ch < 0) {
            return BASE64_VALUE_INVALID;
        }
        return _asciiToBase64[ch];
    }

    /*
    /**********************************************************
    /* Encoding support
    /**********************************************************
     */

    public char encodeBase64BitsAsChar(int value)
    {
        // Let's assume caller has done necessary checks; this
        // method must be fast and inlinable
        return _base64ToAsciiC[value];
    }

    /**
     * Method that encodes given right-aligned (LSB) 24-bit value
     * into 4 base64 characters, stored in given result buffer.
     * Caller must ensure there is sufficient space for 4 encoded characters
     * at specified position.
     *
     * @param b24 3-byte value to encode
     * @param buffer Output buffer to append characters to
     * @param outPtr Starting position within {@code buffer} to append encoded characters
     *
     * @return Pointer in output buffer after appending 4 encoded characters
     */
    public int encodeBase64Chunk(int b24, char[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiC[(b24 >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiC[(b24 >> 12) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiC[(b24 >> 6) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiC[b24 & 0x3F];
        return outPtr;
    }

    public void encodeBase64Chunk(StringBuilder sb, int b24)
    {
        sb.append(_base64ToAsciiC[(b24 >> 18) & 0x3F]);
        sb.append(_base64ToAsciiC[(b24 >> 12) & 0x3F]);
        sb.append(_base64ToAsciiC[(b24 >> 6) & 0x3F]);
        sb.append(_base64ToAsciiC[b24 & 0x3F]);
    }

    /**
     * Method that outputs partial chunk (which only encodes one
     * or two bytes of data). Data given is still aligned same as if
     * it as full data; that is, missing data is at the "right end"
     * (LSB) of int.
     *
     * @param bits 24-bit chunk containing 1 or 2 bytes to encode
     * @param outputBytes Number of input bytes to encode (either 1 or 2)
     * @param buffer Output buffer to append characters to
     * @param outPtr Starting position within {@code buffer} to append encoded characters
     *
     * @return Pointer in output buffer after appending encoded characters (2, 3 or 4)
     */
    public int encodeBase64Partial(int bits, int outputBytes, char[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiC[(bits >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiC[(bits >> 12) & 0x3F];
        if (usesPadding()) {
            buffer[outPtr++] = (outputBytes == 2) ?
                _base64ToAsciiC[(bits >> 6) & 0x3F] : _paddingChar;
            buffer[outPtr++] = _paddingChar;
        } else {
            if (outputBytes == 2) {
                buffer[outPtr++] = _base64ToAsciiC[(bits >> 6) & 0x3F];
            }
        }
        return outPtr;
    }

    public void encodeBase64Partial(StringBuilder sb, int bits, int outputBytes)
    {
        sb.append(_base64ToAsciiC[(bits >> 18) & 0x3F]);
        sb.append(_base64ToAsciiC[(bits >> 12) & 0x3F]);
        if (usesPadding()) {
            sb.append((outputBytes == 2) ?
                      _base64ToAsciiC[(bits >> 6) & 0x3F] : _paddingChar);
            sb.append(_paddingChar);
        } else {
            if (outputBytes == 2) {
                sb.append(_base64ToAsciiC[(bits >> 6) & 0x3F]);
            }
        }
    }

    public byte encodeBase64BitsAsByte(int value)
    {
        // As with above, assuming it is 6-bit value
        return _base64ToAsciiB[value];
    }

    /**
     * Method that encodes given right-aligned (LSB) 24-bit value
     * into 4 base64 bytes (ascii), stored in given result buffer.
     *
     * @param b24 3-byte value to encode
     * @param buffer Output buffer to append characters (as bytes) to
     * @param outPtr Starting position within {@code buffer} to append encoded characters
     *
     * @return Pointer in output buffer after appending 4 encoded characters
     */
    public int encodeBase64Chunk(int b24, byte[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiB[(b24 >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiB[(b24 >> 12) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiB[(b24 >> 6) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiB[b24 & 0x3F];
        return outPtr;
    }

    /**
     * Method that outputs partial chunk (which only encodes one
     * or two bytes of data). Data given is still aligned same as if
     * it as full data; that is, missing data is at the "right end"
     * (LSB) of int.
     *
     * @param bits 24-bit chunk containing 1 or 2 bytes to encode
     * @param outputBytes Number of input bytes to encode (either 1 or 2)
     * @param buffer Output buffer to append characters to
     * @param outPtr Starting position within {@code buffer} to append encoded characters
     *
     * @return Pointer in output buffer after appending encoded characters (2, 3 or 4)
     */
    public int encodeBase64Partial(int bits, int outputBytes, byte[] buffer, int outPtr)
    {
        buffer[outPtr++] = _base64ToAsciiB[(bits >> 18) & 0x3F];
        buffer[outPtr++] = _base64ToAsciiB[(bits >> 12) & 0x3F];
        if (usesPadding()) {
            byte pb = (byte) _paddingChar;
            buffer[outPtr++] = (outputBytes == 2) ?
                _base64ToAsciiB[(bits >> 6) & 0x3F] : pb;
            buffer[outPtr++] = pb;
        } else {
            if (outputBytes == 2) {
                buffer[outPtr++] = _base64ToAsciiB[(bits >> 6) & 0x3F];
            }
        }
        return outPtr;
    }

    /*
    /**********************************************************
    /* Convenience conversion methods for String to/from bytes use case
    /**********************************************************
     */

    /**
     * Convenience method for converting given byte array as base64 encoded
     * String using this variant's settings.
     * Resulting value is "raw", that is, not enclosed in double-quotes.
     *
     * @param input Byte array to encode
     *
     * @return Base64 encoded String of encoded {@code input} bytes, not surrounded by quotes
     */
    public String encode(byte[] input)
    {
        return encode(input, false);
    }

    /**
     * Convenience method for converting given byte array as base64 encoded String
     * using this variant's settings, optionally enclosed in double-quotes.
     * Linefeeds added, if needed, are expressed as 2-character JSON (and Java source)
     * escape sequence of backslash + `n`.
     *
     * @param input Byte array to encode
     * @param addQuotes Whether to surround resulting value in double quotes or not
     *
     * @return Base64 encoded String of encoded {@code input} bytes, possibly
     *     surrounded by quotes (if {@code addQuotes} enabled)
     */
    public String encode(byte[] input, boolean addQuotes)
    {
        final int inputEnd = input.length;
        final StringBuilder sb = new StringBuilder(inputEnd + (inputEnd >> 2) + (inputEnd >> 3));
        if (addQuotes) {
            sb.append('"');
        }

        int chunksBeforeLF = getMaxLineLength() >> 2;

        // Ok, first we loop through all full triplets of data:
        int inputPtr = 0;
        int safeInputEnd = inputEnd-3; // to get only full triplets

        while (inputPtr <= safeInputEnd) {
            // First, mash 3 bytes into lsb of 32-bit int
            int b24 = ((int) input[inputPtr++]) << 8;
            b24 |= ((int) input[inputPtr++]) & 0xFF;
            b24 = (b24 << 8) | (((int) input[inputPtr++]) & 0xFF);
            encodeBase64Chunk(sb, b24);
            if (--chunksBeforeLF <= 0) {
                // note: must quote in JSON value, so not really useful...
                sb.append('\\');
                sb.append('n');
                chunksBeforeLF = getMaxLineLength() >> 2;
            }
        }

        // And then we may have 1 or 2 leftover bytes to encode
        int inputLeft = inputEnd - inputPtr; // 0, 1 or 2
        if (inputLeft > 0) { // yes, but do we have room for output?
            int b24 = ((int) input[inputPtr++]) << 16;
            if (inputLeft == 2) {
                b24 |= (((int) input[inputPtr++]) & 0xFF) << 8;
            }
            encodeBase64Partial(sb, b24, inputLeft);
        }

        if (addQuotes) {
            sb.append('"');
        }
        return sb.toString();
    }

    /**
     * Convenience method for converting given byte array as base64 encoded String
     * using this variant's settings, optionally enclosed in double-quotes.
     * Linefeed character to use is passed explicitly.
     *
     * @param input Byte array to encode
     * @param addQuotes Whether to surround resulting value in double quotes or not
     * @param linefeed Linefeed to use for encoded content
     *
     * @return Base64 encoded String of encoded {@code input} bytes
     *
     * @since 2.10
     */
    public String encode(byte[] input, boolean addQuotes, String linefeed)
    {
        final int inputEnd = input.length;
        final StringBuilder sb = new StringBuilder(inputEnd + (inputEnd >> 2) + (inputEnd >> 3));
        if (addQuotes) {
            sb.append('"');
        }

        int chunksBeforeLF = getMaxLineLength() >> 2;

        int inputPtr = 0;
        int safeInputEnd = inputEnd-3;

        while (inputPtr <= safeInputEnd) {
            int b24 = ((int) input[inputPtr++]) << 8;
            b24 |= ((int) input[inputPtr++]) & 0xFF;
            b24 = (b24 << 8) | (((int) input[inputPtr++]) & 0xFF);
            encodeBase64Chunk(sb, b24);
            if (--chunksBeforeLF <= 0) {
                sb.append(linefeed);
                chunksBeforeLF = getMaxLineLength() >> 2;
            }
        }
        int inputLeft = inputEnd - inputPtr;
        if (inputLeft > 0) {
            int b24 = ((int) input[inputPtr++]) << 16;
            if (inputLeft == 2) {
                b24 |= (((int) input[inputPtr++]) & 0xFF) << 8;
            }
            encodeBase64Partial(sb, b24, inputLeft);
        }

        if (addQuotes) {
            sb.append('"');
        }
        return sb.toString();
    }

    /**
     * Convenience method for decoding contents of a Base64-encoded String,
     * using this variant's settings.
     *
     * @param input Base64-encoded input String to decode
     *
     * @return Byte array of decoded contents
     *
     * @since 2.3
     *
     * @throws IllegalArgumentException if input is not valid base64 encoded data
     */
    @SuppressWarnings("resource")
    public byte[] decode(String input) throws IllegalArgumentException
    {
        ByteArrayBuilder b = new ByteArrayBuilder();
        decode(input, b);
        return b.toByteArray();
    }

    /**
     * Convenience method for decoding contents of a Base64-encoded String,
     * using this variant's settings
     * and appending decoded binary data using provided {@link ByteArrayBuilder}.
     *<p>
     * NOTE: builder will NOT be reset before decoding (nor cleared afterwards);
     * assumption is that caller will ensure it is given in proper state, and
     * used as appropriate afterwards.
     *
     * @param str Input to decode
     * @param builder Builder used for assembling decoded content
     *
     * @since 2.3
     *
     * @throws IllegalArgumentException if input is not valid base64 encoded data
     */
    public void decode(String str, ByteArrayBuilder builder) throws IllegalArgumentException
    {
        int ptr = 0;
        int len = str.length();

    main_loop:
        while (true) {
            // first, we'll skip preceding white space, if any
            char ch;
            do {
                if (ptr >= len) {
                    break main_loop;
                }
                ch = str.charAt(ptr++);
            } while (ch <= INT_SPACE);
            int bits = decodeBase64Char(ch);
            if (bits < 0) {
                _reportInvalidBase64(ch, 0, null);
            }
            int decodedData = bits;
            // then second base64 char; can't get padding yet, nor ws
            if (ptr >= len) {
                _reportBase64EOF();
            }
            ch = str.charAt(ptr++);
            bits = decodeBase64Char(ch);
            if (bits < 0) {
                _reportInvalidBase64(ch, 1, null);
            }
            decodedData = (decodedData << 6) | bits;
            // third base64 char; can be padding, but not ws
            if (ptr >= len) {
                // but as per [JACKSON-631] can be end-of-input, iff padding is not required
                if (!requiresPaddingOnRead()) {
                    decodedData >>= 4;
                    builder.append(decodedData);
                    break;
                }
                _reportBase64EOF();
            }
            ch = str.charAt(ptr++);
            bits = decodeBase64Char(ch);

            // First branch: can get padding (-> 1 byte)
            if (bits < 0) {
                if (bits != Base64Variant.BASE64_VALUE_PADDING) {
                    _reportInvalidBase64(ch, 2, null);
                }
                if (!acceptsPaddingOnRead()) {
                    _reportBase64UnexpectedPadding();
                }
                // Ok, must get padding
                if (ptr >= len) {
                    _reportBase64EOF();
                }
                ch = str.charAt(ptr++);
                if (!usesPaddingChar(ch)) {
                    _reportInvalidBase64(ch, 3, "expected padding character '"+getPaddingChar()+"'");
                }
                // Got 12 bits, only need 8, need to shift
                decodedData >>= 4;
                builder.append(decodedData);
                continue;
            }
            // Nope, 2 or 3 bytes
            decodedData = (decodedData << 6) | bits;
            // fourth and last base64 char; can be padding, but not ws
            if (ptr >= len) {
                // but as per [JACKSON-631] can be end-of-input, iff padding on read is not required
                if (!requiresPaddingOnRead()) {
                    decodedData >>= 2;
                    builder.appendTwoBytes(decodedData);
                    break;
                }
                _reportBase64EOF();
            }
            ch = str.charAt(ptr++);
            bits = decodeBase64Char(ch);
            if (bits < 0) {
                if (bits != Base64Variant.BASE64_VALUE_PADDING) {
                    _reportInvalidBase64(ch, 3, null);
                }
                if (!acceptsPaddingOnRead()) {
                    _reportBase64UnexpectedPadding();
                }
                decodedData >>= 2;
                builder.appendTwoBytes(decodedData);
            } else {
                // otherwise, our triple is now complete
                decodedData = (decodedData << 6) | bits;
                builder.appendThreeBytes(decodedData);
            }
        }
    }

    /*
    /**********************************************************
    /* Overridden standard methods
    /**********************************************************
     */

    @Override
    public String toString() { return _name; }

    @Override
    public boolean equals(Object o) {
        // identity comparison should be fine
        // 26-Oct-2020, tatu: ... not any more with 2.12
        if (o == this) return true;
        if (o == null || o.getClass() != getClass()) return false;

        Base64Variant other = (Base64Variant) o;
        return (other._paddingChar == _paddingChar)
                && (other._maxLineLength == _maxLineLength)
                && (other._writePadding == _writePadding)
                && (other._paddingReadBehaviour == _paddingReadBehaviour)
                && (_name.equals(other._name))
                ;
    }

    @Override
    public int hashCode() {
        return _name.hashCode();
    }

    /*
    /**********************************************************
    /* Internal helper methods
    /**********************************************************
     */

    /**
     * @param ch Character to report on
     * @param bindex Relative index within base64 character unit; between 0
     *   and 3 (as unit has exactly 4 characters)
     * @param msg Base message to use for exception
     */
    protected void _reportInvalidBase64(char ch, int bindex, String msg)
        throws IllegalArgumentException
    {
        String base;
        if (ch <= INT_SPACE) {
            base = "Illegal white space character (code 0x"+Integer.toHexString(ch)+") as character #"+(bindex+1)+" of 4-char base64 unit: can only used between units";
        } else if (usesPaddingChar(ch)) {
            base = "Unexpected padding character ('"+getPaddingChar()+"') as character #"+(bindex+1)+" of 4-char base64 unit: padding only legal as 3rd or 4th character";
        } else if (!Character.isDefined(ch) || Character.isISOControl(ch)) {
            // Not sure if we can really get here... ? (most illegal xml chars are caught at lower level)
            base = "Illegal character (code 0x"+Integer.toHexString(ch)+") in base64 content";
        } else {
            base = "Illegal character '"+ch+"' (code 0x"+Integer.toHexString(ch)+") in base64 content";
        }
        if (msg != null) {
            base = base + ": " + msg;
        }
        throw new IllegalArgumentException(base);
    }

    protected void _reportBase64EOF() throws IllegalArgumentException {
        throw new IllegalArgumentException(missingPaddingMessage());
    }

    protected void _reportBase64UnexpectedPadding() throws IllegalArgumentException {
        throw new IllegalArgumentException(unexpectedPaddingMessage());
    }

    /**
     * Helper method that will construct a message to use in exceptions for cases where input ends
     * prematurely in place where padding is not expected.
     *
     * @return Exception message for indicating "unexpected padding" case
     *
     * @since 2.12
     */
    protected String unexpectedPaddingMessage() {
        return String.format("Unexpected end of base64-encoded String: base64 variant '%s' expects no padding at the end while decoding. This Base64Variant might have been incorrectly configured",
                getName());
    }

    /**
     * Helper method that will construct a message to use in exceptions for cases where input ends
     * prematurely in place where padding would be expected.
     *
     * @return Exception message for indicating "missing padding" case
     *
     * @since 2.10
     */
    public String missingPaddingMessage() { // !!! TODO: why is this 'public'?
        return String.format("Unexpected end of base64-encoded String: base64 variant '%s' expects padding (one or more '%c' characters) at the end. This Base64Variant might have been incorrectly configured",
                getName(), getPaddingChar());
    }
}