Permalink
Fetching contributors…
Cannot retrieve contributors at this time
2062 lines (1748 sloc) 63.1 KB
// 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">$(D $2)</a>
*/
module std.base64;
import std.exception; // enforce
import std.range.primitives; // isInputRange, isOutputRange, isForwardRange, ElementType, hasLength
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);
}
/**
* Template for implementing Base64 encoding and decoding.
*
* For most purposes, direct usage of this template is not necessary; instead,
* this module provides two default implementations: $(LREF Base64) and
* $(LREF Base64URL), that implement basic Base64 encoding and a variant
* intended for use in URLs and filenames, 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 $(D Padding) parameter is
* set to $(D 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[encodeLength(data.length)];
Base64.encode(data, buf);
assert(buf == "Gis8TV1u");
}
// ubyte[] to char[]
/**
* Encode $(D_PARAM source) into a $(D char[]) buffer using Base64
* encoding.
*
* Params:
* source = The $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _encode.
* buffer = The $(D 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");
}
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];
}
///
@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");
}
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 an
* $(LINK2 std_range_primitives.html#isOutputRange, output range) using
* Base64 encoding.
*
* Params:
* source = The $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _encode.
* range = The $(LINK2 std_range_primitives.html#isOutputRange, output
* range) to store the encoded result.
*
* Returns:
* The number of times the output range's $(D put) method was invoked.
*/
size_t encode(R1, R2)(in R1 source, auto ref R2 range)
if (isArray!R1 && is(ElementType!R1 : ubyte) &&
!is(R2 == char[]) && isOutputRange!(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;
}
///
@system unittest
{
// @system because encode for OutputRange is @system
struct OutputRange
{
char[] result;
void put(const(char) ch) @safe { result ~= ch; }
}
ubyte[] data = [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e];
// This overload of encode() returns the number of calls to the output
// range's put method.
OutputRange output;
assert(Base64.encode(data, output) == 8);
assert(output.result == "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))
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 newly-allocated buffer.
*
* This convenience method alleviates the need to manually manage output
* buffers.
*
* Params:
* source = The $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _encode.
*
* Returns:
* A newly-allocated $(D 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 $(LINK2 std_range_primitives.html#isInputRange, input range) that
* iterates over the respective Base64 encodings of a range of data items.
*
* This range will be a $(LINK2 std_range_primitives.html#isForwardRange,
* forward range) 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:
* $(D Base64Exception) If invoked when
* $(LREF2 .Base64Impl.Encoder.empty, empty) returns $(D 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
* $(LINK2 std_range_primitives.html#isForwardRange, forward
* range).
*
* 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_);
}
}
/**
* An $(LINK2 std_range_primitives.html#isInputRange, input range) that
* iterates over the encoded bytes of the given source data.
*
* It will be a $(LINK2 std_range_primitives.html#isForwardRange, forward
* range) 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:
* $(D Base64Exception) If invoked when $(LREF2 .Base64Impl.Encoder.empty.2,
* empty) returns $(D 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
* $(LINK2 std_range_primitives.html#isForwardRange, forward
* range).
*
* Returns:
* A copy of $(D this).
*/
@property
typeof(this) save()
{
auto encoder = this;
encoder.range_ = encoder.range_.save;
return encoder;
}
}
}
/**
* Construct an $(D Encoder) that iterates over the Base64 encoding of the
* given $(LINK2 std_range_primitives.html#isInputRange, input range).
*
* Params:
* range = An $(LINK2 std_range_primitives.html#isInputRange, input
* range) over the data to be encoded.
*
* Returns:
* If $(D_PARAM range) is a range of bytes, an $(D Encoder) that iterates
* over the bytes of the corresponding Base64 encoding.
*
* If $(D_PARAM range) is a range of ranges of bytes, an $(D Encoder) that
* iterates over the Base64 encoded strings of each element of the range.
*
* In both cases, the returned $(D Encoder) will be a
* $(LINK2 std_range_primitives.html#isForwardRange, forward range) if the
* given $(D 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 $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _decode.
* buffer = The buffer to store decoded result.
*
* Returns:
* The slice of $(D_PARAM buffer) containing the decoded result.
*
* Throws:
* $(D 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");
}
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];
}
///
@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");
}
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 a given
* $(LINK2 std_range_primitives.html#isOutputRange, output range).
*
* Params:
* source = The $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _decode.
* range = The $(LINK2 std_range_primitives.html#isOutputRange, output
* range) to store the decoded result.
*
* Returns:
* The number of times the output range's $(D put) method was invoked.
*
* Throws:
* $(D 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");
}
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;
}
///
@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");
}
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 newly-allocated buffer.
*
* This convenience method alleviates the need to manually manage decoding
* buffers.
*
* Params:
* source = The $(LINK2 std_range_primitives.html#isInputRange, input
* range) to _decode.
*
* Returns:
* A newly-allocated $(D 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 $(LINK2 std_range_primitives.html#isInputRange, input range) that
* iterates over the decoded data of a range of Base64 encodings.
*
* This range will be a $(LINK2 std_range_primitives.html#isForwardRange,
* forward range) 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:
* $(D Base64Exception) if invoked when $(LREF2 .Base64Impl.Decoder.empty,
* empty) returns $(D 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
* $(LINK2 std_range_primitives.html#isForwardRange, forward
* range).
*
* 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_);
}
}
/**
* An $(LINK2 std_range_primitives.html#isInputRange, input range) that
* iterates over the bytes of data decoded from a Base64 encoded string.
*
* This range will be a $(LINK2 std_range_primitives.html#isForwardRange,
* forward range) 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:
* $(D Base64Exception) if invoked when $(LREF2 .Base64Impl.Decoder.empty,
* empty) returns $(D 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
* $(LINK2 std_range_primitives.html#isForwardRange, forward
* range).
*
* Returns: A copy of $(D this).
*/
@property
typeof(this) save()
{
auto decoder = this;
decoder.range_ = decoder.range_.save;
return decoder;
}
}
}
/**
* Construct a $(D Decoder) that iterates over the decoding of the given
* Base64 encoded data.
*
* Params:
* range = An $(LINK2 std_range_primitives.html#isInputRange, input
* range) over the data to be decoded.
*
* Returns:
* If $(D_PARAM range) is a range of characters, a $(D Decoder) that
* iterates over the bytes of the corresponding Base64 decoding.
*
* If $(D_PARAM range) is a range of ranges of characters, a $(D 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 $(D Decoder) will be a
* $(LINK2 std_range_primitives.html#isForwardRange, forward range) if the
* given $(D 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
* $(D 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);
}
}
/**
* 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);
}
}
@system unittest
{
import std.algorithm.sorting : sort;
import std.algorithm.comparison : equal;
import std.conv;
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();
}
// @@@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.
@system 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;
assert(Base64.encode(ir2, &or2) == 8);
assert(or2.result == "Gis8TV1u");
}
// Regression control for the output range ref bug in decode.
@system 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;
assert(Base64.decode(ir2, &or2) == 6);
assert(or2.result == [0x1a, 0x2b, 0x3c, 0x4d, 0x5d, 0x6e]);
}