// Written in the D programming language. /** * Encoding / Decoding Base64 format. * * Implemented according to $(WEB tools.ietf.org/html/rfc4648, * RFC 4648 - The Base16, Base32, and Base64 Data Encodings). * * 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]); * ----- * * Support Range interface using Encoder / Decoder. * * 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); * ----- * * Copyright: Masahiro Nakagawa 2010-. * License: $(WEB 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) */ module std.base64; import std.exception; // enforce import std.range; // isInputRange, isOutputRange, isForwardRange, ElementType, hasLength import std.traits; // isArray version(unittest) import std.algorithm, std.conv, std.file, std.stdio; /** * The Base64 */ alias Base64 = Base64Impl!('+', '/'); /** * The "URL and Filename safe" Base64 */ alias Base64URL = Base64Impl!('-', '_'); /** * Core implementation for Base64 format. * * Example: * ----- * alias Base64 = Base64Impl!('+', '/'); // The Base64 format(Already defined). * alias Base64Re = Base64Impl!('!', '=', Base64.NoPadding); // non-standard Base64 format for Regular expression * ----- * * NOTE: * encoded-string doesn't have padding character if set Padding parameter 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 minimum length for encoding. * * Params: * sourceLength = the length of source array. * * Returns: * the calculated length using $(D_PARAM sourceLength). */ @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; } // ubyte[] to char[] /** * Encodes $(D_PARAM source) into $(D_PARAM buffer). * * Params: * source = an $(D InputRange) to encode. * buffer = a buffer to store encoded result. * * Returns: * the encoded string that slices buffer. */ @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"); } body { 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]; } // 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"); } body { 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 (unittest) 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 $(D_PARAM range). * * Params: * source = an $(D InputRange) to encode. * range = an $(D OutputRange) to put encoded result. * * Returns: * the number of calling put. */ size_t encode(R1, R2)(in R1 source, R2 range) if (isArray!R1 && is(ElementType!R1 : ubyte) && !is(R2 == char[])) out(result) { assert(result == encodeLength(source.length), "The number of put is different from the length of Base64"); } body { immutable srcLen = source.length; if (srcLen == 0) return 0; immutable blocks = srcLen / 3; immutable remain = srcLen % 3; auto srcptr = source.ptr; size_t pcount; foreach (Unused; 0..blocks) { immutable val = srcptr[0] << 16 | srcptr[1] << 8 | srcptr[2]; put(range, EncodeMap[val >> 18 ]); put(range, EncodeMap[val >> 12 & 0x3f]); put(range, EncodeMap[val >> 6 & 0x3f]); put(range, EncodeMap[val & 0x3f]); srcptr += 3; pcount += 4; } if (remain) { immutable val = srcptr[0] << 16 | (remain == 2 ? srcptr[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; } // InputRange to OutputRange /** * ditto */ size_t encode(R1, R2)(R1 source, R2 range) if (!isArray!R1 && isInputRange!R1 && is(ElementType!R1 : ubyte) && hasLength!R1 && !is(R2 == char[]) && isOutputRange!(R2, char)) out(result) { // @@@BUG@@@ Workaround for DbC problem. //assert(result == encodeLength(source.length), "The number of put is different from the length of Base64"); } body { 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 (unittest) assert(pcount == encodeLength(srcLen), "The number of put is different from the length of Base64"); return pcount; } /** * Encodes $(D_PARAM source) to new buffer. * * Shortcut to encode(source, buffer) function. */ @safe pure char[] encode(Range)(Range source) if (isArray!Range && is(ElementType!Range : ubyte)) { return encode(source, new char[encodeLength(source.length)]); } /** * 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)]); } /** * Range that encodes chunk data at a time. */ 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(); } /** * Range primitive operation that checks iteration state. * * Returns: * true if there are no more elements to be iterated. */ @property @trusted bool empty() { return range_.empty; } /** * Range primitive operation that returns the currently iterated element. * * Returns: * the encoded string. */ @property @safe nothrow char[] front() { return encoded_; } /** * Range primitive operation that advances the range to its next element. * * Throws: * an Exception when you try to call popFront on empty range. */ 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) { /** * Captures a Range state. * * Returns: * a copy of $(D 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_); } } /** * Range that encodes single character at a time. */ 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(); } /** * Range primitive operation that checks iteration state. * * Returns: * true if there are no more elements to be iterated. */ @property @safe nothrow bool empty() const { static if (Padding == NoPadding) return pos < 0; else return pos < 0 && !padding; } /** * Range primitive operation that returns the currently iterated element. * * Returns: * the encoded character. */ @property @safe nothrow ubyte front() { return first; } /** * Range primitive operation that advances the range to its next element. * * Throws: * an Exception when you try to call popFront on empty range. */ 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) { /** * Captures a Range state. * * Returns: * a copy of $(D this). */ @property typeof(this) save() { auto encoder = this; encoder.range_ = encoder.range_.save; return encoder; } } } /** * Iterates through an $(D InputRange) at a time by using $(D Encoder). * * Default $(D Encoder) encodes chunk data. * * Example: * ----- * File f = File("text.txt", "r"); * scope(exit) f.close(); * * uint line = 0; * foreach (encoded; Base64.encoder(f.byLine())) * { * writeln(++line, ". ", encoded); * } * ----- * * In addition, You can use $(D Encoder) that returns encoded single character. * This $(D Encoder) performs Range-based and lazy encoding. * * Example: * ----- * ubyte[] data = cast(ubyte[]) "0123456789"; * * // The ElementType of data is not aggregation type * foreach (encoded; Base64.encoder(data)) * { * writeln(encoded); * } * ----- * * Params: * range = an $(D InputRange) to iterate. * * Returns: * a $(D Encoder) object instantiated and initialized according to the arguments. */ 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 ]; /** * Calculates the minimum length for decoding. * * Params: * sourceLength = the length of source array. * * Returns: * calculated length using $(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; } // 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 $(D_PARAM buffer). * * Params: * source = an $(D InputRange) to decode. * buffer = a buffer to store decoded result. * * Returns: * the decoded string that slices buffer. * * Throws: * an Exception if $(D_PARAM source) has character outside base-alphabet. */ @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 >= decodeLength(source.length), "Insufficient buffer for decoding"); } out(result) { immutable expect = realDecodeLength(source); assert(result.length == expect, "The length of result is different from the expected length"); } body { 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]; } // 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"); } out(result) { // @@@BUG@@@ Workaround for DbC problem. //immutable expect = decodeLength(source.length) - 2; //assert(result.length >= expect, "The length of result is smaller than expected length"); } body { 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); } } } // @@@BUG@@@ Workaround for DbC problem. version (unittest) 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 $(D_PARAM range). * * Params: * source = an $(D InputRange) to decode. * range = an $(D OutputRange) to put decoded result * * Returns: * the number of calling put. * * Throws: * an Exception if $(D_PARAM source) has character outside base-alphabet. */ size_t decode(R1, R2)(in R1 source, 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"); } body { 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; } // InputRange to OutputRange /** * ditto */ size_t decode(R1, R2)(R1 source, 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"); } body { 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 (unittest) assert(pcount >= (decodeLength(srcLen) - 2), "The length of result is smaller than expected length"); return pcount; } /** * Decodes $(D_PARAM source) into new buffer. * * Shortcut to decode(source, buffer) function. */ @safe pure ubyte[] decode(Range)(Range source) if (isArray!Range && is(ElementType!Range : dchar)) { return decode(source, new ubyte[decodeLength(source.length)]); } /** * 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)]); } /** * Range that decodes chunk data at a time. */ 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(); } /** * Range primitive operation that checks iteration state. * * Returns: * true if there are no more elements to be iterated. */ @property @trusted bool empty() { return range_.empty; } /** * Range primitive operation that returns the currently iterated element. * * Returns: * the decoded result. */ @property @safe nothrow ubyte[] front() { return decoded_; } /** * Range primitive operation that advances the range to its next element. * * Throws: * an Exception when you try to call popFront on empty range. */ 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) { /** * Captures a Range state. * * Returns: * a copy of $(D 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_); } } /** * Range that decodes single character at a time. */ 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(); } /** * Range primitive operation that checks iteration state. * * Returns: * true if there are no more elements to be iterated. */ @property @safe nothrow bool empty() const { return pos < 0; } /** * Range primitive operation that returns the currently iterated element. * * Returns: * the decoded result. */ @property @safe nothrow ubyte front() { return first; } /** * Range primitive operation that advances the range to its next element. * * Throws: * an Exception when you try to call popFront on empty range. */ 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) { /** * Captures a Range state. * * Returns: * a copy of $(D this). */ @property typeof(this) save() { auto decoder = this; decoder.range_ = decoder.range_.save; return decoder; } } } /** * Iterates through an $(D InputRange) at a time by using $(D Decoder). * * Default $(D Decoder) decodes chunk data. * * Example: * ----- * foreach (decoded; Base64.decoder(stdin.byLine())) * { * writeln(decoded); * } * ----- * * In addition, You can use $(D Decoder) that returns decoded single character. * This $(D Decoder) performs Range-based and lazy decoding. * * Example: * ----- * auto encoded = Base64.encoder(cast(ubyte[])"0123456789"); * foreach (n; map!q{a - '0'}(Base64.decoder(encoded))) * { * writeln(n); * } * ----- * * NOTE: * If you use $(D ByChunk), chunk-size should be the multiple of 4. * $(D Decoder) can't judge a encode-boundary. * * Params: * range = an $(D InputRange) to iterate. * * Returns: * a $(D Decoder) object instantiated and initialized according to the arguments. */ 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); } } /** * Exception thrown on Base64 errors. */ class Base64Exception : Exception { @safe pure nothrow this(string s, string fn = __FILE__, size_t ln = __LINE__) { super(s, fn, ln); } } unittest { 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 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(); } // @@@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 { std.file.write("testingEncoder", "\nf\nfo\nfoo\nfoob\nfooba\nfoobar"); auto witness = ["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"]; auto f = File("testingEncoder"); scope(exit) { f.close(); assert(!f.isOpen); std.file.remove("testingEncoder"); } size_t i; foreach (encoded; Base64.encoder(f.byLine())) assert(encoded == witness[i++]); assert(i == witness.length); } { std.file.write("testingDecoder", "\nZg==\nZm8=\nZm9v\nZm9vYg==\nZm9vYmE=\nZm9vYmFy"); auto witness = tv.keys.sort; auto f = File("testingDecoder"); scope(exit) { f.close(); assert(!f.isOpen); std.file.remove("testingDecoder"); } size_t i; foreach (decoded; Base64.decoder(f.byLine())) assert(decoded == witness[i++]); assert(i == witness.length); } { // ForwardRange { auto encoder = Base64.encoder(tv.values.sort); 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 = tv.values.sort; 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)); } } }