// Written in the D programming language.

/**
 * Support for Base64 encoding and decoding.
 *
 * This module provides two default implementations of Base64 encoding,
 * $(LREF Base64) with a standard encoding alphabet, and a variant
 * $(LREF Base64URL) that has a modified encoding alphabet designed to be
 * safe for embedding in URLs and filenames.
 *
 * Both variants are implemented as instantiations of the template
 * $(LREF Base64Impl). Most users will not need to use this template
 * directly; however, it can be used to create customized Base64 encodings,
 * such as one that omits padding characters, or one that is safe to embed
 * inside a regular expression.
 *
 * Example:
 * -----
 * ubyte[] data = [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e];
 *
 * const(char)[] encoded = Base64.encode(data);
 * assert(encoded == "FPucA9l+");
 *
 * ubyte[] decoded = Base64.decode("FPucA9l+");
 * assert(decoded == [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]);
 * -----
 *
 * The range API is supported for both encoding and decoding:
 *
 * Example:
 * -----
 * // Create MIME Base64 with CRLF, per line 76.
 * File f = File("./text.txt", "r");
 * scope(exit) f.close();
 *
 * Appender!string mime64 = appender!string;
 *
 * foreach (encoded; Base64.encoder(f.byChunk(57)))
 * {
 *     mime64.put(encoded);
 *     mime64.put("\r\n");
 * }
 *
 * writeln(mime64.data);
 * -----
 *
 * References:
 * $(LINK2 https://tools.ietf.org/html/rfc4648, RFC 4648 - The Base16, Base32, and Base64
 * Data Encodings)
 *
 * Copyright: Masahiro Nakagawa 2010-.
 * License:   $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).
 * Authors:   Masahiro Nakagawa, Daniel Murphy (Single value Encoder and Decoder)
 * Source:    $(PHOBOSSRC std/base64.d)
 * Macros:
 *      LREF2=<a href="#$1">`$2`</a>
 */
module std.base64;

import std.exception : enforce;
import std.range.primitives : empty, front, isInputRange, isOutputRange,
    isForwardRange, ElementType, hasLength, popFront, put, save;
import std.traits : isArray;

// Make sure module header code examples work correctly.
@safe unittest
{
    ubyte[] data = [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e];

    const(char)[] encoded = Base64.encode(data);
    assert(encoded == "FPucA9l+");

    ubyte[] decoded = Base64.decode("FPucA9l+");
    assert(decoded == [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]);
}

/**
 * Implementation of standard _Base64 encoding.
 *
 * See $(LREF Base64Impl) for a description of available methods.
 */
alias Base64 = Base64Impl!('+', '/');

///
@safe unittest
{
    ubyte[] data = [0x83, 0xd7, 0x30, 0x7a, 0x01, 0x3f];
    assert(Base64.encode(data) == "g9cwegE/");
    assert(Base64.decode("g9cwegE/") == data);
}


/**
 * Variation of Base64 encoding that is safe for use in URLs and filenames.
 *
 * See $(LREF Base64Impl) for a description of available methods.
 */
alias Base64URL = Base64Impl!('-', '_');

///
@safe unittest
{
    ubyte[] data = [0x83, 0xd7, 0x30, 0x7a, 0x01, 0x3f];
    assert(Base64URL.encode(data) == "g9cwegE_");
    assert(Base64URL.decode("g9cwegE_") == data);
}

/**
 * Unpadded variation of Base64 encoding that is safe for use in URLs and
 * filenames, as used in RFCs 4648 and 7515 (JWS/JWT/JWE).
 *
 * See $(LREF Base64Impl) for a description of available methods.
 */
alias Base64URLNoPadding = Base64Impl!('-', '_', Base64.NoPadding);

///
@safe unittest
{
    ubyte[] data = [0x83, 0xd7, 0x30, 0x7b, 0xef];
    assert(Base64URLNoPadding.encode(data) == "g9cwe-8");
    assert(Base64URLNoPadding.decode("g9cwe-8") == data);
}

/**
 * Template for implementing Base64 encoding and decoding.
 *
 * For most purposes, direct usage of this template is not necessary; instead,
 * this module provides default implementations: $(LREF Base64), implementing
 * basic Base64 encoding, and $(LREF Base64URL) and $(LREF Base64URLNoPadding),
 * that implement the Base64 variant for use in URLs and filenames, with
 * and without padding, respectively.
 *
 * Customized Base64 encoding schemes can be implemented by instantiating this
 * template with the appropriate arguments. For example:
 *
 * -----
 * // Non-standard Base64 format for embedding in regular expressions.
 * alias Base64Re = Base64Impl!('!', '=', Base64.NoPadding);
 * -----
 *
 * NOTE:
 * Encoded strings will not have any padding if the `Padding` parameter is
 * set to `NoPadding`.
 */
template Base64Impl(char Map62th, char Map63th, char Padding = '=')
{
    enum NoPadding = '\0';  /// represents no-padding encoding


    // Verify Base64 characters
    static assert(Map62th < 'A' || Map62th > 'Z', "Character '" ~ Map62th ~ "' cannot be used twice");
    static assert(Map63th < 'A' || Map63th > 'Z', "Character '" ~ Map63th ~ "' cannot be used twice");
    static assert(Padding < 'A' || Padding > 'Z', "Character '" ~ Padding ~ "' cannot be used twice");
    static assert(Map62th < 'a' || Map62th > 'z', "Character '" ~ Map62th ~ "' cannot be used twice");
    static assert(Map63th < 'a' || Map63th > 'z', "Character '" ~ Map63th ~ "' cannot be used twice");
    static assert(Padding < 'a' || Padding > 'z', "Character '" ~ Padding ~ "' cannot be used twice");
    static assert(Map62th < '0' || Map62th > '9', "Character '" ~ Map62th ~ "' cannot be used twice");
    static assert(Map63th < '0' || Map63th > '9', "Character '" ~ Map63th ~ "' cannot be used twice");
    static assert(Padding < '0' || Padding > '9', "Character '" ~ Padding ~ "' cannot be used twice");
    static assert(Map62th != Map63th, "Character '" ~ Map63th ~ "' cannot be used twice");
    static assert(Map62th != Padding, "Character '" ~ Padding ~ "' cannot be used twice");
    static assert(Map63th != Padding, "Character '" ~ Padding ~ "' cannot be used twice");
    static assert(Map62th != NoPadding, "'\\0' is not a valid Base64character");
    static assert(Map63th != NoPadding, "'\\0' is not a valid Base64character");


    /* Encode functions */


    private immutable EncodeMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ~ Map62th ~ Map63th;


    /**
     * Calculates the length needed to store the encoded string corresponding
     * to an input of the given length.
     *
     * Params:
     *  sourceLength = Length of the source array.
     *
     * Returns:
     *  The length of a Base64 encoding of an array of the given length.
     */
    @safe
    pure nothrow size_t encodeLength(in size_t sourceLength)
    {
        static if (Padding == NoPadding)
            return (sourceLength / 3) * 4 + (sourceLength % 3 == 0 ? 0 : sourceLength % 3 == 1 ? 2 : 3);
        else
            return (sourceLength / 3 + (sourceLength % 3 ? 1 : 0)) * 4;
    }

    ///
    @safe unittest
    {
        ubyte[] data = [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e];

        // Allocate a buffer large enough to hold the encoded string.
        auto buf = new char[Base64.encodeLength(data.length)];

        Base64.encode(data, buf);
        assert(buf == "Gis8TV1u");
    }


    // ubyte[] to char[]


    /**
     * Encode $(D_PARAM source) into a `char[]` buffer using Base64
     * encoding.
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _encode.
     *  buffer = The `char[]` buffer to store the encoded result.
     *
     * Returns:
     *  The slice of $(D_PARAM buffer) that contains the encoded string.
     */
    @trusted
    pure char[] encode(R1, R2)(in R1 source, R2 buffer) if (isArray!R1 && is(ElementType!R1 : ubyte) &&
                                                            is(R2 == char[]))
    in
    {
        assert(buffer.length >= encodeLength(source.length), "Insufficient buffer for encoding");
    }
    out(result)
    {
        assert(result.length == encodeLength(source.length), "The length of result is different from Base64");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return [];

        immutable blocks = srcLen / 3;
        immutable remain = srcLen % 3;
        auto      bufptr = buffer.ptr;
        auto      srcptr = source.ptr;

        foreach (Unused; 0 .. blocks)
        {
            immutable val = srcptr[0] << 16 | srcptr[1] << 8 | srcptr[2];
            *bufptr++ = EncodeMap[val >> 18       ];
            *bufptr++ = EncodeMap[val >> 12 & 0x3f];
            *bufptr++ = EncodeMap[val >>  6 & 0x3f];
            *bufptr++ = EncodeMap[val       & 0x3f];
            srcptr += 3;
        }

        if (remain)
        {
            immutable val = srcptr[0] << 16 | (remain == 2 ? srcptr[1] << 8 : 0);
            *bufptr++ = EncodeMap[val >> 18       ];
            *bufptr++ = EncodeMap[val >> 12 & 0x3f];

            final switch (remain)
            {
            case 2:
                *bufptr++ = EncodeMap[val >> 6 & 0x3f];
                static if (Padding != NoPadding)
                    *bufptr++ = Padding;
                break;
            case 1:
                static if (Padding != NoPadding)
                {
                    *bufptr++ = Padding;
                    *bufptr++ = Padding;
                }
                break;
            }
        }

        // encode method can't assume buffer length. So, slice needed.
        return buffer[0 .. bufptr - buffer.ptr];
    }

    ///
    @safe unittest
    {
        ubyte[] data = [0x83, 0xd7, 0x30, 0x7a, 0x01, 0x3f];
        char[32] buffer;    // much bigger than necessary

        // Just to be sure...
        auto encodedLength = Base64.encodeLength(data.length);
        assert(buffer.length >= encodedLength);

        // encode() returns a slice to the provided buffer.
        auto encoded = Base64.encode(data, buffer[]);
        assert(encoded is buffer[0 .. encodedLength]);
        assert(encoded == "g9cwegE/");
    }


    // InputRange to char[]


    /**
     * ditto
     */
    char[] encode(R1, R2)(R1 source, R2 buffer) if (!isArray!R1 && isInputRange!R1 &&
                                                    is(ElementType!R1 : ubyte) && hasLength!R1 &&
                                                    is(R2 == char[]))
    in
    {
        assert(buffer.length >= encodeLength(source.length), "Insufficient buffer for encoding");
    }
    out(result)
    {
        // @@@BUG@@@ D's DbC can't caputre an argument of function and store the result of precondition.
        //assert(result.length == encodeLength(source.length), "The length of result is different from Base64");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return [];

        immutable blocks = srcLen / 3;
        immutable remain = srcLen % 3;
        auto      bufptr = buffer.ptr;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = source.front; source.popFront();
            immutable v2 = source.front; source.popFront();
            immutable v3 = source.front; source.popFront();
            immutable val = v1 << 16 | v2 << 8 | v3;
            *bufptr++ = EncodeMap[val >> 18       ];
            *bufptr++ = EncodeMap[val >> 12 & 0x3f];
            *bufptr++ = EncodeMap[val >>  6 & 0x3f];
            *bufptr++ = EncodeMap[val       & 0x3f];
        }

        if (remain)
        {
            size_t val = source.front << 16;
            if (remain == 2)
            {
                source.popFront();
                val |= source.front << 8;
            }

            *bufptr++ = EncodeMap[val >> 18       ];
            *bufptr++ = EncodeMap[val >> 12 & 0x3f];

            final switch (remain)
            {
            case 2:
                *bufptr++ = EncodeMap[val >> 6 & 0x3f];
                static if (Padding != NoPadding)
                    *bufptr++ = Padding;
                break;
            case 1:
                static if (Padding != NoPadding)
                {
                    *bufptr++ = Padding;
                    *bufptr++ = Padding;
                }
                break;
            }
        }

        // @@@BUG@@@ Workaround for DbC problem. See comment on 'out'.
        version (StdUnittest)
            assert(
                bufptr - buffer.ptr == encodeLength(srcLen),
                "The length of result is different from Base64"
            );

        // encode method can't assume buffer length. So, slice needed.
        return buffer[0 .. bufptr - buffer.ptr];
    }


    // ubyte[] to OutputRange


    /**
     * Encodes $(D_PARAM source) into an
     * $(REF_ALTTEXT output range, isOutputRange, std,range,primitives) using
     * Base64 encoding.
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _encode.
     *  range  = The $(REF_ALTTEXT output range, isOutputRange, std,range,primitives)
     *           to store the encoded result.
     *
     * Returns:
     *  The number of times the output range's `put` method was invoked.
     */
    size_t encode(E, R)(scope const(E)[] source, auto ref R range)
    if (is(E : ubyte) && isOutputRange!(R, char) && !is(R == char[]))
    out(result)
    {
        assert(result == encodeLength(source.length), "The number of put is different from the length of Base64");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return 0;

        immutable blocks = srcLen / 3;
        immutable remain = srcLen % 3;
        auto s = source; // copy for out contract length check
        size_t pcount;

        foreach (Unused; 0 .. blocks)
        {
            immutable val = s[0] << 16 | s[1] << 8 | s[2];
            put(range, EncodeMap[val >> 18       ]);
            put(range, EncodeMap[val >> 12 & 0x3f]);
            put(range, EncodeMap[val >>  6 & 0x3f]);
            put(range, EncodeMap[val       & 0x3f]);
            s = s[3 .. $];
            pcount += 4;
        }

        if (remain)
        {
            immutable val = s[0] << 16 | (remain == 2 ? s[1] << 8 : 0);
            put(range, EncodeMap[val >> 18       ]);
            put(range, EncodeMap[val >> 12 & 0x3f]);
            pcount += 2;

            final switch (remain)
            {
            case 2:
                put(range, EncodeMap[val >> 6 & 0x3f]);
                pcount++;

                static if (Padding != NoPadding)
                {
                    put(range, Padding);
                    pcount++;
                }
                break;
            case 1:
                static if (Padding != NoPadding)
                {
                    put(range, Padding);
                    put(range, Padding);
                    pcount += 2;
                }
                break;
            }
        }

        return pcount;
    }

    ///
    @safe pure nothrow unittest
    {
        import std.array : appender;

        auto output = appender!string();
        ubyte[] data = [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e];

        // This overload of encode() returns the number of calls to the output
        // range's put method.
        assert(Base64.encode(data, output) == 8);
        assert(output.data == "Gis8TV1u");
    }


    // InputRange to OutputRange


    /**
     * ditto
     */
    size_t encode(R1, R2)(R1 source, auto ref R2 range)
        if (!isArray!R1 && isInputRange!R1 && is(ElementType!R1 : ubyte) &&
            hasLength!R1 && !is(R2 == char[]) && isOutputRange!(R2, char))
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return 0;

        immutable blocks = srcLen / 3;
        immutable remain = srcLen % 3;
        size_t    pcount;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = source.front; source.popFront();
            immutable v2 = source.front; source.popFront();
            immutable v3 = source.front; source.popFront();
            immutable val = v1 << 16 | v2 << 8 | v3;
            put(range, EncodeMap[val >> 18       ]);
            put(range, EncodeMap[val >> 12 & 0x3f]);
            put(range, EncodeMap[val >>  6 & 0x3f]);
            put(range, EncodeMap[val       & 0x3f]);
            pcount += 4;
        }

        if (remain)
        {
            size_t val = source.front << 16;
            if (remain == 2)
            {
                source.popFront();
                val |= source.front << 8;
            }

            put(range, EncodeMap[val >> 18       ]);
            put(range, EncodeMap[val >> 12 & 0x3f]);
            pcount += 2;

            final switch (remain)
            {
            case 2:
                put(range, EncodeMap[val >> 6 & 0x3f]);
                pcount++;

                static if (Padding != NoPadding)
                {
                    put(range, Padding);
                    pcount++;
                }
                break;
            case 1:
                static if (Padding != NoPadding)
                {
                    put(range, Padding);
                    put(range, Padding);
                    pcount += 2;
                }
                break;
            }
        }

        // @@@BUG@@@ Workaround for DbC problem.
        version (StdUnittest)
            assert(
                pcount == encodeLength(srcLen),
                "The number of put is different from the length of Base64"
            );

        return pcount;
    }


    /**
     * Encodes $(D_PARAM source) to newly-allocated buffer.
     *
     * This convenience method alleviates the need to manually manage output
     * buffers.
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _encode.
     *
     * Returns:
     *  A newly-allocated `char[]` buffer containing the encoded string.
     */
    @safe
    pure char[] encode(Range)(Range source) if (isArray!Range && is(ElementType!Range : ubyte))
    {
        return encode(source, new char[encodeLength(source.length)]);
    }

    ///
    @safe unittest
    {
        ubyte[] data = [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e];
        assert(Base64.encode(data) == "Gis8TV1u");
    }


    /**
     * ditto
     */
    char[] encode(Range)(Range source) if (!isArray!Range && isInputRange!Range &&
                                           is(ElementType!Range : ubyte) && hasLength!Range)
    {
        return encode(source, new char[encodeLength(source.length)]);
    }


    /**
     * An $(REF_ALTTEXT input range, isInputRange, std,range,primitives) that
     * iterates over the respective Base64 encodings of a range of data items.
     *
     * This range will be a $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
     * if the underlying data source is at least a forward range.
     *
     * Note: This struct is not intended to be created in user code directly;
     * use the $(LREF encoder) function instead.
     */
    struct Encoder(Range) if (isInputRange!Range && (is(ElementType!Range : const(ubyte)[]) ||
                                                     is(ElementType!Range : const(char)[])))
    {
      private:
        Range  range_;
        char[] buffer_, encoded_;


      public:
        this(Range range)
        {
            range_ = range;
            doEncoding();
        }


        /**
         * Returns:
         *  true if there is no more encoded data left.
         */
        @property @trusted
        bool empty()
        {
            return range_.empty;
        }


        /**
         * Returns: The current chunk of encoded data.
         */
        @property @safe
        nothrow char[] front()
        {
            return encoded_;
        }


        /**
         * Advance the range to the next chunk of encoded data.
         *
         * Throws:
         *  `Base64Exception` If invoked when
         *  $(LREF2 .Base64Impl.Encoder.empty, empty) returns `true`.
         */
        void popFront()
        {
            enforce(!empty, new Base64Exception("Cannot call popFront on Encoder with no data remaining"));

            range_.popFront();

            /*
             * This check is very ugly. I think this is a Range's flaw.
             * I very strongly want the Range guideline for unified implementation.
             *
             * In this case, Encoder becomes a beautiful implementation if 'front' performs Base64 encoding.
             */
            if (!empty)
                doEncoding();
        }


        static if (isForwardRange!Range)
        {
            /**
             * Save the current iteration state of the range.
             *
             * This method is only available if the underlying range is a
             * $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives).
             *
             * Returns:
             *  A copy of `this`.
             */
            @property
            typeof(this) save()
            {
                typeof(return) encoder;

                encoder.range_   = range_.save;
                encoder.buffer_  = buffer_.dup;
                encoder.encoded_ = encoder.buffer_[0 .. encoded_.length];

                return encoder;
            }
        }


      private:
        void doEncoding()
        {
            auto data = cast(const(ubyte)[])range_.front;
            auto size = encodeLength(data.length);
            if (size > buffer_.length)
                buffer_.length = size;

            encoded_ = encode(data, buffer_);
        }
    }


    /**
     * An $(REF_ALTTEXT input range, isInputRange, std,range,primitives) that
     * iterates over the encoded bytes of the given source data.
     *
     * It will be a $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
     * if the underlying data source is at least a forward range.
     *
     * Note: This struct is not intended to be created in user code directly;
     * use the $(LREF encoder) function instead.
     */
    struct Encoder(Range) if (isInputRange!Range && is(ElementType!Range : ubyte))
    {
      private:
        Range range_;
        ubyte first;
        int   pos, padding;


      public:
        this(Range range)
        {
            range_ = range;
            static if (isForwardRange!Range)
                range_ = range_.save;

            if (range_.empty)
                pos = -1;
            else
                popFront();
        }


        /**
         * Returns:
         *  true if there are no more encoded characters to be iterated.
         */
        @property @safe
        nothrow bool empty() const
        {
            static if (Padding == NoPadding)
                return pos < 0;
            else
                return pos < 0 && !padding;
        }


        /**
         * Returns: The current encoded character.
         */
        @property @safe
        nothrow ubyte front()
        {
            return first;
        }


        /**
         * Advance to the next encoded character.
         *
         * Throws:
         *  `Base64Exception` If invoked when $(LREF2 .Base64Impl.Encoder.empty.2,
         *  empty) returns `true`.
         */
        void popFront()
        {
            enforce(!empty, new Base64Exception("Cannot call popFront on Encoder with no data remaining"));

            static if (Padding != NoPadding)
                if (padding)
                {
                    first = Padding;
                    pos   = -1;
                    padding--;
                    return;
                }

            if (range_.empty)
            {
                pos = -1;
                return;
            }

            final switch (pos)
            {
            case 0:
                first = EncodeMap[range_.front >> 2];
                break;
            case 1:
                immutable t = (range_.front & 0b11) << 4;
                range_.popFront();

                if (range_.empty)
                {
                    first   = EncodeMap[t];
                    padding = 3;
                }
                else
                {
                    first = EncodeMap[t | (range_.front >> 4)];
                }
                break;
            case 2:
                immutable t = (range_.front & 0b1111) << 2;
                range_.popFront();

                if (range_.empty)
                {
                    first   = EncodeMap[t];
                    padding = 2;
                }
                else
                {
                    first = EncodeMap[t | (range_.front >> 6)];
                }
                break;
            case 3:
                first = EncodeMap[range_.front & 0b111111];
                range_.popFront();
                break;
            }

            ++pos %= 4;
        }


        static if (isForwardRange!Range)
        {
            /**
             * Save the current iteration state of the range.
             *
             * This method is only available if the underlying range is a
             * $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives).
             *
             * Returns:
             *  A copy of `this`.
             */
            @property
            typeof(this) save()
            {
                auto encoder = this;
                encoder.range_ = encoder.range_.save;
                return encoder;
            }
        }
    }


    /**
     * Construct an `Encoder` that iterates over the Base64 encoding of the
     * given $(REF_ALTTEXT input range, isInputRange, std,range,primitives).
     *
     * Params:
     *  range = An $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *          over the data to be encoded.
     *
     * Returns:
     *  If $(D_PARAM range) is a range of bytes, an `Encoder` that iterates
     *  over the bytes of the corresponding Base64 encoding.
     *
     *  If $(D_PARAM range) is a range of ranges of bytes, an `Encoder` that
     *  iterates over the Base64 encoded strings of each element of the range.
     *
     *  In both cases, the returned `Encoder` will be a
     *  $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) if the
     *  given `range` is at least a forward range, otherwise it will be only
     *  an input range.
     *
     * Example:
     * This example encodes the input one line at a time.
     * -----
     * File f = File("text.txt", "r");
     * scope(exit) f.close();
     *
     * uint line = 0;
     * foreach (encoded; Base64.encoder(f.byLine()))
     * {
     *     writeln(++line, ". ", encoded);
     * }
     * -----
     *
     * Example:
     * This example encodes the input data one byte at a time.
     * -----
     * ubyte[] data = cast(ubyte[]) "0123456789";
     *
     * // The ElementType of data is not aggregation type
     * foreach (encoded; Base64.encoder(data))
     * {
     *     writeln(encoded);
     * }
     * -----
     */
    Encoder!(Range) encoder(Range)(Range range) if (isInputRange!Range)
    {
        return typeof(return)(range);
    }


    /* Decode functions */


    private immutable int[char.max + 1] DecodeMap = [
        'A':0b000000, 'B':0b000001, 'C':0b000010, 'D':0b000011, 'E':0b000100,
        'F':0b000101, 'G':0b000110, 'H':0b000111, 'I':0b001000, 'J':0b001001,
        'K':0b001010, 'L':0b001011, 'M':0b001100, 'N':0b001101, 'O':0b001110,
        'P':0b001111, 'Q':0b010000, 'R':0b010001, 'S':0b010010, 'T':0b010011,
        'U':0b010100, 'V':0b010101, 'W':0b010110, 'X':0b010111, 'Y':0b011000,
        'Z':0b011001, 'a':0b011010, 'b':0b011011, 'c':0b011100, 'd':0b011101,
        'e':0b011110, 'f':0b011111, 'g':0b100000, 'h':0b100001, 'i':0b100010,
        'j':0b100011, 'k':0b100100, 'l':0b100101, 'm':0b100110, 'n':0b100111,
        'o':0b101000, 'p':0b101001, 'q':0b101010, 'r':0b101011, 's':0b101100,
        't':0b101101, 'u':0b101110, 'v':0b101111, 'w':0b110000, 'x':0b110001,
        'y':0b110010, 'z':0b110011, '0':0b110100, '1':0b110101, '2':0b110110,
        '3':0b110111, '4':0b111000, '5':0b111001, '6':0b111010, '7':0b111011,
        '8':0b111100, '9':0b111101, Map62th:0b111110, Map63th:0b111111, Padding:-1
    ];


    /**
     * Given a Base64 encoded string, calculates the length of the decoded
     * string.
     *
     * Params:
     *  sourceLength = The length of the Base64 encoding.
     *
     * Returns:
     *  The length of the decoded string corresponding to a Base64 encoding of
     *  length $(D_PARAM sourceLength).
     */
    @safe
    pure nothrow size_t decodeLength(in size_t sourceLength)
    {
        static if (Padding == NoPadding)
            return (sourceLength / 4) * 3 + (sourceLength % 4 < 2 ? 0 : sourceLength % 4 == 2 ? 1 : 2);
        else
            return (sourceLength / 4) * 3;
    }

    ///
    @safe unittest
    {
        auto encoded = "Gis8TV1u";

        // Allocate a sufficiently large buffer to hold to decoded result.
        auto buffer = new ubyte[Base64.decodeLength(encoded.length)];

        Base64.decode(encoded, buffer);
        assert(buffer == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
    }


    // Used in decode contracts. Calculates the actual size the decoded
    // result should have, taking into account trailing padding.
    @safe
    pure nothrow private size_t realDecodeLength(R)(R source)
    {
        auto expect = decodeLength(source.length);
        static if (Padding != NoPadding)
        {
            if (source.length % 4 == 0)
            {
                expect -= source.length == 0       ? 0 :
                          source[$ - 2] == Padding ? 2 :
                          source[$ - 1] == Padding ? 1 : 0;
            }
        }
        return expect;
    }


    // char[] to ubyte[]


    /**
     * Decodes $(D_PARAM source) into the given buffer.
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _decode.
     *  buffer = The buffer to store decoded result.
     *
     * Returns:
     *  The slice of $(D_PARAM buffer) containing the decoded result.
     *
     * Throws:
     *  `Base64Exception` if $(D_PARAM source) contains characters outside the
     *  base alphabet of the current Base64 encoding scheme.
     */
    @trusted
    pure ubyte[] decode(R1, R2)(in R1 source, R2 buffer) if (isArray!R1 && is(ElementType!R1 : dchar) &&
                                                             is(R2 == ubyte[]) && isOutputRange!(R2, ubyte))
    in
    {
        assert(buffer.length >= realDecodeLength(source), "Insufficient buffer for decoding");
    }
    out(result)
    {
        immutable expect = realDecodeLength(source);
        assert(result.length == expect, "The length of result is different from the expected length");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return [];
        static if (Padding != NoPadding)
            enforce(srcLen % 4 == 0, new Base64Exception("Invalid length of encoded data"));

        immutable blocks = srcLen / 4;
        auto      srcptr = source.ptr;
        auto      bufptr = buffer.ptr;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = decodeChar(*srcptr++);
            immutable v2 = decodeChar(*srcptr++);

            *bufptr++ = cast(ubyte)(v1 << 2 | v2 >> 4);

            immutable v3 = decodeChar(*srcptr++);
            if (v3 == -1)
                break;

            *bufptr++ = cast(ubyte)((v2 << 4 | v3 >> 2) & 0xff);

            immutable v4 = decodeChar(*srcptr++);
            if (v4 == -1)
                break;

            *bufptr++ = cast(ubyte)((v3 << 6 | v4) & 0xff);
        }

        static if (Padding == NoPadding)
        {
            immutable remain = srcLen % 4;

            if (remain)
            {
                immutable v1 = decodeChar(*srcptr++);
                immutable v2 = decodeChar(*srcptr++);

                *bufptr++ = cast(ubyte)(v1 << 2 | v2 >> 4);

                if (remain == 3)
                    *bufptr++ = cast(ubyte)((v2 << 4 | decodeChar(*srcptr++) >> 2) & 0xff);
            }
        }

        return buffer[0 .. bufptr - buffer.ptr];
    }

    ///
    @safe unittest
    {
        auto encoded = "Gis8TV1u";
        ubyte[32] buffer;   // much bigger than necessary

        // Just to be sure...
        auto decodedLength = Base64.decodeLength(encoded.length);
        assert(buffer.length >= decodedLength);

        // decode() returns a slice of the given buffer.
        auto decoded = Base64.decode(encoded, buffer[]);
        assert(decoded is buffer[0 .. decodedLength]);
        assert(decoded == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
    }

    // InputRange to ubyte[]


    /**
     * ditto
     */
    ubyte[] decode(R1, R2)(R1 source, R2 buffer) if (!isArray!R1 && isInputRange!R1 &&
                                                     is(ElementType!R1 : dchar) && hasLength!R1 &&
                                                     is(R2 == ubyte[]) && isOutputRange!(R2, ubyte))
    in
    {
        assert(buffer.length >= decodeLength(source.length), "Insufficient buffer for decoding");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return [];
        static if (Padding != NoPadding)
            enforce(srcLen % 4 == 0, new Base64Exception("Invalid length of encoded data"));

        immutable blocks = srcLen / 4;
        auto      bufptr = buffer.ptr;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = decodeChar(source.front); source.popFront();
            immutable v2 = decodeChar(source.front); source.popFront();

            *bufptr++ = cast(ubyte)(v1 << 2 | v2 >> 4);

            immutable v3 = decodeChar(source.front);
            if (v3 == -1)
                break;

            *bufptr++ = cast(ubyte)((v2 << 4 | v3 >> 2) & 0xff);
            source.popFront();

            immutable v4 = decodeChar(source.front);
            if (v4 == -1)
                break;

            *bufptr++ = cast(ubyte)((v3 << 6 | v4) & 0xff);
            source.popFront();
        }

        static if (Padding == NoPadding)
        {
            immutable remain = srcLen % 4;

            if (remain)
            {
                immutable v1 = decodeChar(source.front); source.popFront();
                immutable v2 = decodeChar(source.front);

                *bufptr++ = cast(ubyte)(v1 << 2 | v2 >> 4);

                if (remain == 3)
                {
                    source.popFront();
                    *bufptr++ = cast(ubyte)((v2 << 4 | decodeChar(source.front) >> 2) & 0xff);
                }
            }
        }

        // We need to do the check here because we have consumed the length
        version (StdUnittest)
            assert(
                (bufptr - buffer.ptr) >= (decodeLength(srcLen) - 2),
                "The length of result is smaller than expected length"
            );

        return buffer[0 .. bufptr - buffer.ptr];
    }


    // char[] to OutputRange


    /**
     * Decodes $(D_PARAM source) into a given
     * $(REF_ALTTEXT output range, isOutputRange, std,range,primitives).
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _decode.
     *  range  = The $(REF_ALTTEXT output range, isOutputRange, std,range,primitives)
     *           to store the decoded result.
     *
     * Returns:
     *  The number of times the output range's `put` method was invoked.
     *
     * Throws:
     *  `Base64Exception` if $(D_PARAM source) contains characters outside the
     *  base alphabet of the current Base64 encoding scheme.
     */
    size_t decode(R1, R2)(in R1 source, auto ref R2 range)
        if (isArray!R1 && is(ElementType!R1 : dchar) &&
            !is(R2 == ubyte[]) && isOutputRange!(R2, ubyte))
    out(result)
    {
        immutable expect = realDecodeLength(source);
        assert(result == expect, "The result of decode is different from the expected");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return 0;
        static if (Padding != NoPadding)
            enforce(srcLen % 4 == 0, new Base64Exception("Invalid length of encoded data"));

        immutable blocks = srcLen / 4;
        auto      srcptr = source.ptr;
        size_t    pcount;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = decodeChar(*srcptr++);
            immutable v2 = decodeChar(*srcptr++);

            put(range, cast(ubyte)(v1 << 2 | v2 >> 4));
            pcount++;

            immutable v3 = decodeChar(*srcptr++);
            if (v3 == -1)
                break;

            put(range, cast(ubyte)((v2 << 4 | v3 >> 2) & 0xff));
            pcount++;

            immutable v4 = decodeChar(*srcptr++);
            if (v4 == -1)
                break;

            put(range, cast(ubyte)((v3 << 6 | v4) & 0xff));
            pcount++;
        }

        static if (Padding == NoPadding)
        {
            immutable remain = srcLen % 4;

            if (remain)
            {
                immutable v1 = decodeChar(*srcptr++);
                immutable v2 = decodeChar(*srcptr++);

                put(range, cast(ubyte)(v1 << 2 | v2 >> 4));
                pcount++;

                if (remain == 3)
                {
                    put(range, cast(ubyte)((v2 << 4 | decodeChar(*srcptr++) >> 2) & 0xff));
                    pcount++;
                }
            }
        }

        return pcount;
    }

    ///
    @system unittest
    {
        struct OutputRange
        {
            ubyte[] result;
            void put(ubyte b) { result ~= b; }
        }
        OutputRange output;

        // This overload of decode() returns the number of calls to put().
        assert(Base64.decode("Gis8TV1u", output) == 6);
        assert(output.result == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
    }


    // InputRange to OutputRange


    /**
     * ditto
     */
    size_t decode(R1, R2)(R1 source, auto ref R2 range)
        if (!isArray!R1 && isInputRange!R1 && is(ElementType!R1 : dchar) &&
            hasLength!R1 && !is(R2 == ubyte[]) && isOutputRange!(R2, ubyte))
    out(result)
    {
        // @@@BUG@@@ Workaround for DbC problem.
        //immutable expect = decodeLength(source.length) - 2;
        //assert(result >= expect, "The length of result is smaller than expected length");
    }
    do
    {
        immutable srcLen = source.length;
        if (srcLen == 0)
            return 0;
        static if (Padding != NoPadding)
            enforce(srcLen % 4 == 0, new Base64Exception("Invalid length of encoded data"));

        immutable blocks = srcLen / 4;
        size_t    pcount;

        foreach (Unused; 0 .. blocks)
        {
            immutable v1 = decodeChar(source.front); source.popFront();
            immutable v2 = decodeChar(source.front); source.popFront();

            put(range, cast(ubyte)(v1 << 2 | v2 >> 4));
            pcount++;

            immutable v3 = decodeChar(source.front);
            if (v3 == -1)
                break;

            put(range, cast(ubyte)((v2 << 4 | v3 >> 2) & 0xff));
            source.popFront();
            pcount++;

            immutable v4 = decodeChar(source.front);
            if (v4 == -1)
                break;

            put(range, cast(ubyte)((v3 << 6 | v4) & 0xff));
            source.popFront();
            pcount++;
        }

        static if (Padding == NoPadding)
        {
            immutable remain = srcLen % 4;

            if (remain)
            {
                immutable v1 = decodeChar(source.front); source.popFront();
                immutable v2 = decodeChar(source.front);

                put(range, cast(ubyte)(v1 << 2 | v2 >> 4));
                pcount++;

                if (remain == 3)
                {
                    source.popFront();
                    put(range, cast(ubyte)((v2 << 4 | decodeChar(source.front) >> 2) & 0xff));
                    pcount++;
                }
            }
        }

        // @@@BUG@@@ Workaround for DbC problem.
        version (StdUnittest)
            assert(
                pcount >= (decodeLength(srcLen) - 2),
                "The length of result is smaller than expected length"
            );

        return pcount;
    }


    /**
     * Decodes $(D_PARAM source) into newly-allocated buffer.
     *
     * This convenience method alleviates the need to manually manage decoding
     * buffers.
     *
     * Params:
     *  source = The $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *           to _decode.
     *
     * Returns:
     *  A newly-allocated `ubyte[]` buffer containing the decoded string.
     */
    @safe
    pure ubyte[] decode(Range)(Range source) if (isArray!Range && is(ElementType!Range : dchar))
    {
        return decode(source, new ubyte[decodeLength(source.length)]);
    }

    ///
    @safe unittest
    {
        auto data = "Gis8TV1u";
        assert(Base64.decode(data) == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
    }


    /**
     * ditto
     */
    ubyte[] decode(Range)(Range source) if (!isArray!Range && isInputRange!Range &&
                                            is(ElementType!Range : dchar) && hasLength!Range)
    {
        return decode(source, new ubyte[decodeLength(source.length)]);
    }


    /**
     * An $(REF_ALTTEXT input range, isInputRange, std,range,primitives) that
     * iterates over the decoded data of a range of Base64 encodings.
     *
     * This range will be a $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
     * if the underlying data source is at least a forward range.
     *
     * Note: This struct is not intended to be created in user code directly;
     * use the $(LREF decoder) function instead.
     */
    struct Decoder(Range) if (isInputRange!Range && (is(ElementType!Range : const(char)[]) ||
                                                     is(ElementType!Range : const(ubyte)[])))
    {
      private:
        Range   range_;
        ubyte[] buffer_, decoded_;


      public:
        this(Range range)
        {
            range_ = range;
            doDecoding();
        }


        /**
         * Returns:
         *  true if there are no more elements to be iterated.
         */
        @property @trusted
        bool empty()
        {
            return range_.empty;
        }


        /**
         * Returns: The decoding of the current element in the input.
         */
        @property @safe
        nothrow ubyte[] front()
        {
            return decoded_;
        }


        /**
         * Advance to the next element in the input to be decoded.
         *
         * Throws:
         *  `Base64Exception` if invoked when $(LREF2 .Base64Impl.Decoder.empty,
         *  empty) returns `true`.
         */
        void popFront()
        {
            enforce(!empty, new Base64Exception("Cannot call popFront on Decoder with no data remaining."));

            range_.popFront();

            /*
             * I mentioned Encoder's popFront.
             */
            if (!empty)
                doDecoding();
        }


        static if (isForwardRange!Range)
        {
            /**
             * Saves the current iteration state.
             *
             * This method is only available if the underlying range is a
             * $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
             *
             * Returns: A copy of `this`.
             */
            @property
            typeof(this) save()
            {
                typeof(return) decoder;

                decoder.range_   = range_.save;
                decoder.buffer_  = buffer_.dup;
                decoder.decoded_ = decoder.buffer_[0 .. decoded_.length];

                return decoder;
            }
        }


      private:
        void doDecoding()
        {
            auto data = cast(const(char)[])range_.front;

            static if (Padding == NoPadding)
            {
                while (data.length % 4 == 1)
                {
                    range_.popFront();
                    data ~= cast(const(char)[])range_.front;
                }
            }
            else
            {
                while (data.length % 4 != 0)
                {
                    range_.popFront();
                    data ~= cast(const(char)[])range_.front;
                }
            }

            auto size = decodeLength(data.length);
            if (size > buffer_.length)
                buffer_.length = size;

            decoded_ = decode(data, buffer_);
        }
    }


    /**
     * An $(REF_ALTTEXT input range, isInputRange, std,range,primitives) that
     * iterates over the bytes of data decoded from a Base64 encoded string.
     *
     * This range will be a $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
     * if the underlying data source is at least a forward range.
     *
     * Note: This struct is not intended to be created in user code directly;
     * use the $(LREF decoder) function instead.
     */
    struct Decoder(Range) if (isInputRange!Range && is(ElementType!Range : char))
    {
      private:
        Range range_;
        ubyte first;
        int   pos;


      public:
        this(Range range)
        {
            range_ = range;
            static if (isForwardRange!Range)
                range_ = range_.save;

            static if (Padding != NoPadding && hasLength!Range)
                enforce(range_.length % 4 == 0, new Base64Exception("Invalid length of encoded data"));

            if (range_.empty)
                pos = -1;
            else
                popFront();
        }


        /**
         * Returns:
         *  true if there are no more elements to be iterated.
         */
        @property @safe
        nothrow bool empty() const
        {
            return pos < 0;
        }


        /**
         * Returns: The current decoded byte.
         */
        @property @safe
        nothrow ubyte front()
        {
            return first;
        }


        /**
         * Advance to the next decoded byte.
         *
         * Throws:
         *  `Base64Exception` if invoked when $(LREF2 .Base64Impl.Decoder.empty,
         *  empty) returns `true`.
         */
        void popFront()
        {
            enforce(!empty, new Base64Exception("Cannot call popFront on Decoder with no data remaining"));

            static if (Padding == NoPadding)
            {
                bool endCondition()
                {
                    return range_.empty;
                }
            }
            else
            {
                bool endCondition()
                {
                    enforce(!range_.empty, new Base64Exception("Missing padding"));
                    return range_.front == Padding;
                }
            }

            if (range_.empty || range_.front == Padding)
            {
                pos = -1;
                return;
            }

            final switch (pos)
            {
            case 0:
                enforce(!endCondition(), new Base64Exception("Premature end of data found"));

                immutable t = DecodeMap[range_.front] << 2;
                range_.popFront();

                enforce(!endCondition(), new Base64Exception("Premature end of data found"));
                first = cast(ubyte)(t | (DecodeMap[range_.front] >> 4));
                break;
            case 1:
                immutable t = (DecodeMap[range_.front] & 0b1111) << 4;
                range_.popFront();

                if (endCondition())
                {
                    pos = -1;
                    return;
                }
                else
                {
                    first = cast(ubyte)(t | (DecodeMap[range_.front] >> 2));
                }
                break;
            case 2:
                immutable t = (DecodeMap[range_.front] & 0b11) << 6;
                range_.popFront();

                if (endCondition())
                {
                    pos = -1;
                    return;
                }
                else
                {
                    first = cast(ubyte)(t | DecodeMap[range_.front]);
                }

                range_.popFront();
                break;
            }

            ++pos %= 3;
        }


        static if (isForwardRange!Range)
        {
            /**
             * Saves the current iteration state.
             *
             * This method is only available if the underlying range is a
             * $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives)
             *
             * Returns: A copy of `this`.
             */
            @property
            typeof(this) save()
            {
                auto decoder = this;
                decoder.range_ = decoder.range_.save;
                return decoder;
            }
        }
    }


    /**
     * Construct a `Decoder` that iterates over the decoding of the given
     * Base64 encoded data.
     *
     * Params:
     *  range = An $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
     *      over the data to be decoded.
     *
     * Returns:
     *  If $(D_PARAM range) is a range of characters, a `Decoder` that
     *  iterates over the bytes of the corresponding Base64 decoding.
     *
     *  If $(D_PARAM range) is a range of ranges of characters, a `Decoder`
     *  that iterates over the decoded strings corresponding to each element of
     *  the range. In this case, the length of each subrange must be a multiple
     *  of 4; the returned _decoder does not keep track of Base64 decoding
     *  state across subrange boundaries.
     *
     *  In both cases, the returned `Decoder` will be a
     * $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) if the
     *  given `range` is at least a forward range, otherwise it will be only
     *  an input range.
     *
     * If the input data contains characters not found in the base alphabet of
     * the current Base64 encoding scheme, the returned range may throw a
     * `Base64Exception`.
     *
     * Example:
     * This example shows decoding over a range of input data lines.
     * -----
     * foreach (decoded; Base64.decoder(stdin.byLine()))
     * {
     *     writeln(decoded);
     * }
     * -----
     *
     * Example:
     * This example shows decoding one byte at a time.
     * -----
     * auto encoded = Base64.encoder(cast(ubyte[])"0123456789");
     * foreach (n; map!q{a - '0'}(Base64.decoder(encoded)))
     * {
     *     writeln(n);
     * }
     * -----
     */
    Decoder!(Range) decoder(Range)(Range range) if (isInputRange!Range)
    {
        return typeof(return)(range);
    }


  private:
    @safe
    pure int decodeChar()(char chr)
    {
        immutable val = DecodeMap[chr];

        // enforce can't be a pure function, so I use trivial check.
        if (val == 0 && chr != 'A')
            throw new Base64Exception("Invalid character: " ~ chr);

        return val;
    }


    @safe
    pure int decodeChar()(dchar chr)
    {
        // See above comment.
        if (chr > 0x7f)
            throw new Base64Exception("Base64-encoded character must be a single byte");

        return decodeChar(cast(char) chr);
    }
}

///
@safe unittest
{
    import std.string : representation;

    // pre-defined: alias Base64 = Base64Impl!('+', '/');
    ubyte[] emptyArr;
    assert(Base64.encode(emptyArr) == "");
    assert(Base64.encode("f".representation) == "Zg==");
    assert(Base64.encode("foo".representation) == "Zm9v");

    alias Base64Re = Base64Impl!('!', '=', Base64.NoPadding);
    assert(Base64Re.encode("f".representation) == "Zg");
    assert(Base64Re.encode("foo".representation) == "Zm9v");
}

/**
 * Exception thrown upon encountering Base64 encoding or decoding errors.
 */
class Base64Exception : Exception
{
    @safe pure nothrow
    this(string s, string fn = __FILE__, size_t ln = __LINE__)
    {
        super(s, fn, ln);
    }
}

///
@safe unittest
{
    import std.exception : assertThrown;
    assertThrown!Base64Exception(Base64.decode("ab|c"));
}

@system unittest
{
    import std.algorithm.comparison : equal;
    import std.algorithm.sorting : sort;
    import std.conv;
    import std.exception : assertThrown;
    import std.file;
    import std.stdio;

    alias Base64Re = Base64Impl!('!', '=', Base64.NoPadding);

    // Test vectors from RFC 4648
    ubyte[][string] tv = [
         ""      :cast(ubyte[])"",
         "f"     :cast(ubyte[])"f",
         "fo"    :cast(ubyte[])"fo",
         "foo"   :cast(ubyte[])"foo",
         "foob"  :cast(ubyte[])"foob",
         "fooba" :cast(ubyte[])"fooba",
         "foobar":cast(ubyte[])"foobar"
    ];

    { // Base64
        // encode
        assert(Base64.encodeLength(tv[""].length)       == 0);
        assert(Base64.encodeLength(tv["f"].length)      == 4);
        assert(Base64.encodeLength(tv["fo"].length)     == 4);
        assert(Base64.encodeLength(tv["foo"].length)    == 4);
        assert(Base64.encodeLength(tv["foob"].length)   == 8);
        assert(Base64.encodeLength(tv["fooba"].length)  == 8);
        assert(Base64.encodeLength(tv["foobar"].length) == 8);

        assert(Base64.encode(tv[""])       == "");
        assert(Base64.encode(tv["f"])      == "Zg==");
        assert(Base64.encode(tv["fo"])     == "Zm8=");
        assert(Base64.encode(tv["foo"])    == "Zm9v");
        assert(Base64.encode(tv["foob"])   == "Zm9vYg==");
        assert(Base64.encode(tv["fooba"])  == "Zm9vYmE=");
        assert(Base64.encode(tv["foobar"]) == "Zm9vYmFy");

        // decode
        assert(Base64.decodeLength(Base64.encode(tv[""]).length)       == 0);
        assert(Base64.decodeLength(Base64.encode(tv["f"]).length)      == 3);
        assert(Base64.decodeLength(Base64.encode(tv["fo"]).length)     == 3);
        assert(Base64.decodeLength(Base64.encode(tv["foo"]).length)    == 3);
        assert(Base64.decodeLength(Base64.encode(tv["foob"]).length)   == 6);
        assert(Base64.decodeLength(Base64.encode(tv["fooba"]).length)  == 6);
        assert(Base64.decodeLength(Base64.encode(tv["foobar"]).length) == 6);

        assert(Base64.decode(Base64.encode(tv[""]))       == tv[""]);
        assert(Base64.decode(Base64.encode(tv["f"]))      == tv["f"]);
        assert(Base64.decode(Base64.encode(tv["fo"]))     == tv["fo"]);
        assert(Base64.decode(Base64.encode(tv["foo"]))    == tv["foo"]);
        assert(Base64.decode(Base64.encode(tv["foob"]))   == tv["foob"]);
        assert(Base64.decode(Base64.encode(tv["fooba"]))  == tv["fooba"]);
        assert(Base64.decode(Base64.encode(tv["foobar"])) == tv["foobar"]);

        assertThrown!Base64Exception(Base64.decode("ab|c"));

        // Test decoding incomplete strings. RFC does not specify the correct
        // behavior, but the code should never throw Errors on invalid input.

        // decodeLength is nothrow
        assert(Base64.decodeLength(1) == 0);
        assert(Base64.decodeLength(2) <= 1);
        assert(Base64.decodeLength(3) <= 2);

        // may throw Exceptions, may not throw Errors
        assertThrown!Base64Exception(Base64.decode("Zg"));
        assertThrown!Base64Exception(Base64.decode("Zg="));
        assertThrown!Base64Exception(Base64.decode("Zm8"));
        assertThrown!Base64Exception(Base64.decode("Zg==;"));
    }

    { // No padding
        // encode
        assert(Base64Re.encodeLength(tv[""].length)       == 0);
        assert(Base64Re.encodeLength(tv["f"].length)      == 2);
        assert(Base64Re.encodeLength(tv["fo"].length)     == 3);
        assert(Base64Re.encodeLength(tv["foo"].length)    == 4);
        assert(Base64Re.encodeLength(tv["foob"].length)   == 6);
        assert(Base64Re.encodeLength(tv["fooba"].length)  == 7);
        assert(Base64Re.encodeLength(tv["foobar"].length) == 8);

        assert(Base64Re.encode(tv[""])       == "");
        assert(Base64Re.encode(tv["f"])      == "Zg");
        assert(Base64Re.encode(tv["fo"])     == "Zm8");
        assert(Base64Re.encode(tv["foo"])    == "Zm9v");
        assert(Base64Re.encode(tv["foob"])   == "Zm9vYg");
        assert(Base64Re.encode(tv["fooba"])  == "Zm9vYmE");
        assert(Base64Re.encode(tv["foobar"]) == "Zm9vYmFy");

        // decode
        assert(Base64Re.decodeLength(Base64Re.encode(tv[""]).length)       == 0);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["f"]).length)      == 1);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["fo"]).length)     == 2);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["foo"]).length)    == 3);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["foob"]).length)   == 4);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["fooba"]).length)  == 5);
        assert(Base64Re.decodeLength(Base64Re.encode(tv["foobar"]).length) == 6);

        assert(Base64Re.decode(Base64Re.encode(tv[""]))       == tv[""]);
        assert(Base64Re.decode(Base64Re.encode(tv["f"]))      == tv["f"]);
        assert(Base64Re.decode(Base64Re.encode(tv["fo"]))     == tv["fo"]);
        assert(Base64Re.decode(Base64Re.encode(tv["foo"]))    == tv["foo"]);
        assert(Base64Re.decode(Base64Re.encode(tv["foob"]))   == tv["foob"]);
        assert(Base64Re.decode(Base64Re.encode(tv["fooba"]))  == tv["fooba"]);
        assert(Base64Re.decode(Base64Re.encode(tv["foobar"])) == tv["foobar"]);

        // decodeLength is nothrow
        assert(Base64.decodeLength(1) == 0);
    }

    { // with OutputRange
        import std.array;

        auto a = Appender!(char[])([]);
        auto b = Appender!(ubyte[])([]);

        assert(Base64.encode(tv[""], a) == 0);
        assert(Base64.decode(a.data, b) == 0);
        assert(tv[""] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["f"], a) == 4);
        assert(Base64.decode(a.data,  b) == 1);
        assert(tv["f"] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["fo"], a) == 4);
        assert(Base64.decode(a.data,   b) == 2);
        assert(tv["fo"] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["foo"], a) == 4);
        assert(Base64.decode(a.data,    b) == 3);
        assert(tv["foo"] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["foob"], a) == 8);
        assert(Base64.decode(a.data,     b) == 4);
        assert(tv["foob"] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["fooba"], a) == 8);
        assert(Base64.decode(a.data, b)      == 5);
        assert(tv["fooba"] == b.data); a.clear(); b.clear();

        assert(Base64.encode(tv["foobar"], a) == 8);
        assert(Base64.decode(a.data, b)       == 6);
        assert(tv["foobar"] == b.data); a.clear(); b.clear();
    }

    // https://issues.dlang.org/show_bug.cgi?id=9543
    // These tests were disabled because they actually relied on the input range having length.
    // The implementation (currently) doesn't support encoding/decoding from a length-less source.
    version (none)
    { // with InputRange
        // InputRange to ubyte[] or char[]
        auto encoded = Base64.encode(map!(to!(ubyte))(["20", "251", "156", "3", "217", "126"]));
        assert(encoded == "FPucA9l+");
        assert(Base64.decode(map!q{a}(encoded)) == [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]);

        // InputRange to OutputRange
        auto a = Appender!(char[])([]);
        auto b = Appender!(ubyte[])([]);
        assert(Base64.encode(map!(to!(ubyte))(["20", "251", "156", "3", "217", "126"]), a) == 8);
        assert(a.data == "FPucA9l+");
        assert(Base64.decode(map!q{a}(a.data), b) == 6);
        assert(b.data == [0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e]);
    }

    { // Encoder and Decoder
        {
            string encode_file = std.file.deleteme ~ "-testingEncoder";
            std.file.write(encode_file, "\nf\nfo\nfoo\nfoob\nfooba\nfoobar");

            auto witness = ["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"];
            auto f = File(encode_file);
            scope(exit)
            {
                f.close();
                assert(!f.isOpen);
                std.file.remove(encode_file);
            }

            size_t i;
            foreach (encoded; Base64.encoder(f.byLine()))
                assert(encoded == witness[i++]);

            assert(i == witness.length);
        }

        {
            string decode_file = std.file.deleteme ~ "-testingDecoder";
            std.file.write(decode_file, "\nZg==\nZm8=\nZm9v\nZm9vYg==\nZm9vYmE=\nZm9vYmFy");

            auto witness = sort(tv.keys);
            auto f = File(decode_file);
            scope(exit)
            {
                f.close();
                assert(!f.isOpen);
                std.file.remove(decode_file);
            }

            size_t i;
            foreach (decoded; Base64.decoder(f.byLine()))
                assert(decoded == witness[i++]);

            assert(i == witness.length);
        }

        { // ForwardRange
            {
                auto encoder = Base64.encoder(sort(tv.values));
                auto witness = ["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"];
                size_t i;

                assert(encoder.front == witness[i++]); encoder.popFront();
                assert(encoder.front == witness[i++]); encoder.popFront();
                assert(encoder.front == witness[i++]); encoder.popFront();

                foreach (encoded; encoder.save)
                    assert(encoded == witness[i++]);
            }

            {
                auto decoder = Base64.decoder(["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"]);
                auto witness = sort(tv.values);
                size_t i;

                assert(decoder.front == witness[i++]); decoder.popFront();
                assert(decoder.front == witness[i++]); decoder.popFront();
                assert(decoder.front == witness[i++]); decoder.popFront();

                foreach (decoded; decoder.save)
                    assert(decoded == witness[i++]);
            }
        }
    }

    { // Encoder and Decoder for single character encoding and decoding
        alias Base64NoPadding = Base64Impl!('+', '/', Base64.NoPadding);

        auto tests = [
            ""       : ["", "", "", ""],
            "f"      : ["Zg==", "Zg==", "Zg", "Zg"],
            "fo"     : ["Zm8=", "Zm8=", "Zm8", "Zm8"],
            "foo"    : ["Zm9v", "Zm9v", "Zm9v", "Zm9v"],
            "foob"   : ["Zm9vYg==", "Zm9vYg==", "Zm9vYg", "Zm9vYg"],
            "fooba"  : ["Zm9vYmE=", "Zm9vYmE=", "Zm9vYmE", "Zm9vYmE"],
            "foobar" : ["Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy", "Zm9vYmFy"],
        ];

        foreach (u, e; tests)
        {
            assert(equal(Base64.encoder(cast(ubyte[]) u), e[0]));
            assert(equal(Base64.decoder(Base64.encoder(cast(ubyte[]) u)), u));

            assert(equal(Base64URL.encoder(cast(ubyte[]) u), e[1]));
            assert(equal(Base64URL.decoder(Base64URL.encoder(cast(ubyte[]) u)), u));

            assert(equal(Base64NoPadding.encoder(cast(ubyte[]) u), e[2]));
            assert(equal(Base64NoPadding.decoder(Base64NoPadding.encoder(cast(ubyte[]) u)), u));

            assert(equal(Base64Re.encoder(cast(ubyte[]) u), e[3]));
            assert(equal(Base64Re.decoder(Base64Re.encoder(cast(ubyte[]) u)), u));
        }
    }
}

// Regression control for the output range ref bug in encode.
@safe unittest
{
    struct InputRange
    {
        ubyte[] impl = [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e];
        @property bool empty() { return impl.length == 0; }
        @property ubyte front() { return impl[0]; }
        void popFront() { impl = impl[1 .. $]; }
        @property size_t length() { return impl.length; }
    }

    struct OutputRange
    {
        char[] result;
        void put(char b) { result ~= b; }
    }

    InputRange ir;
    OutputRange or;
    assert(Base64.encode(ir, or) == 8);
    assert(or.result == "Gis8TV1u");

    // Verify that any existing workaround that uses & still works.
    InputRange ir2;
    OutputRange or2;
    () @trusted {
        assert(Base64.encode(ir2, &or2) == 8);
    }();
    assert(or2.result == "Gis8TV1u");
}

// Regression control for the output range ref bug in decode.
@safe unittest
{
    struct InputRange
    {
        const(char)[] impl = "Gis8TV1u";
        @property bool empty() { return impl.length == 0; }
        @property dchar front() { return impl[0]; }
        void popFront() { impl = impl[1 .. $]; }
        @property size_t length() { return impl.length; }
    }

    struct OutputRange
    {
        ubyte[] result;
        void put(ubyte b) { result ~= b; }
    }

    InputRange ir;
    OutputRange or;
    assert(Base64.decode(ir, or) == 6);
    assert(or.result == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);

    // Verify that any existing workaround that uses & still works.
    InputRange ir2;
    OutputRange or2;
    () @trusted {
        assert(Base64.decode(ir2, &or2) == 6);
    }();
    assert(or2.result == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
}
