mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 13:10:35 +03:00

* Fix UndocumentedDeclarationCheck linting issue * Fix IfConstraintsIndentCheck linting issue * Address feedback * Fix publictests CI * Fix old (libdparse) D-Scanner linting warn
2202 lines
66 KiB
D
2202 lines
66 KiB
D
// 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.
|
|
pure @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!('+', '/');
|
|
|
|
///
|
|
pure @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!('-', '_');
|
|
|
|
///
|
|
pure @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);
|
|
|
|
///
|
|
pure @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 @nogc
|
|
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)(const scope R1 source, return scope 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];
|
|
}
|
|
|
|
///
|
|
@nogc nothrow @safe unittest
|
|
{
|
|
ubyte[6] 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;
|
|
if (!empty)
|
|
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()
|
|
{
|
|
assert(!empty, "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()
|
|
{
|
|
assert(!empty, "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 @nogc 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 @nogc 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, return scope 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;
|
|
if (!empty)
|
|
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()
|
|
{
|
|
assert(!empty, "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();
|
|
enforce(!range_.empty, new Base64Exception("Invalid length of encoded data"));
|
|
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, or a `char` array. Will not accept
|
|
* `wchar[]` nor `dchar[]`.
|
|
*
|
|
* Returns:
|
|
* If $(D_PARAM range) is a range or array of `char`, 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 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);
|
|
* }
|
|
* -----
|
|
*
|
|
* 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);
|
|
}
|
|
|
|
/// ditto
|
|
Decoder!(const(ubyte)[]) decoder()(const(char)[] range)
|
|
{
|
|
import std.string : representation;
|
|
return typeof(return)(range.representation);
|
|
}
|
|
|
|
///
|
|
@safe pure unittest
|
|
{
|
|
import std.algorithm.comparison : equal;
|
|
string encoded =
|
|
"VGhvdSBzaGFsdCBuZXZlciBjb250aW51ZSBhZnRlciBhc3NlcnRpbmcgbnVsbA==";
|
|
|
|
assert(Base64.decoder(encoded)
|
|
.equal("Thou shalt never continue after asserting null"));
|
|
}
|
|
|
|
|
|
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]);
|
|
}
|
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=21679
|
|
// https://issues.dlang.org/show_bug.cgi?id=21706
|
|
@safe unittest
|
|
{
|
|
ubyte[][] input;
|
|
assert(Base64.encoder(input).empty);
|
|
assert(Base64.decoder(input).empty);
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
struct InputRange(ubyte[] data)
|
|
{
|
|
ubyte[] impl = data;
|
|
bool empty() { return impl.length == 0; }
|
|
ubyte front() { return impl[0]; }
|
|
void popFront() { impl = impl[1 .. $]; }
|
|
size_t length() { return impl.length; }
|
|
}
|
|
|
|
struct OutputRange
|
|
{
|
|
ubyte[] result;
|
|
void put(ubyte b) { result ~= b; }
|
|
}
|
|
|
|
void test_encode(ubyte[] data, string result)()
|
|
{
|
|
InputRange!data ir;
|
|
OutputRange or;
|
|
assert(Base64.encode(ir, or) == result.length);
|
|
assert(or.result == result);
|
|
}
|
|
|
|
void test_decode(ubyte[] data, string result)()
|
|
{
|
|
InputRange!data ir;
|
|
OutputRange or;
|
|
assert(Base64.decode(ir, or) == result.length);
|
|
assert(or.result == result);
|
|
}
|
|
|
|
test_encode!([], "");
|
|
test_encode!(['x'], "eA==");
|
|
test_encode!([123, 45], "ey0=");
|
|
|
|
test_decode!([], "");
|
|
test_decode!(['e', 'A', '=', '='], "x");
|
|
test_decode!(['e', 'y', '0', '='], "{-");
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// checking forward range
|
|
auto item = Base64.decoder(Base64.encoder(cast(ubyte[]) "foobar"));
|
|
auto copy = item.save();
|
|
item.popFront();
|
|
assert(item.front == 'o');
|
|
assert(copy.front == 'f');
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// checking invalid dchar
|
|
dchar[] c = cast(dchar[]) "ääää";
|
|
|
|
import std.exception : assertThrown;
|
|
assertThrown!Base64Exception(Base64.decode(c));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
import std.array : array;
|
|
|
|
char[][] input = [['e', 'y'], ['0', '=']];
|
|
assert(Base64.decoder(input).array == [[123, 45]]);
|
|
}
|
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=21707
|
|
@safe unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
char[][] t1 = [[ 'Z', 'g', '=' ]];
|
|
assertThrown!Base64Exception(Base64.decoder(t1));
|
|
|
|
char[][] t2 = [[ 'e', 'y', '0' ], ['=', '=']];
|
|
assertThrown!Base64Exception(Base64.decoder(t2));
|
|
}
|