1 /*******************************************************************************
2 
3     This module is used to decode and encode base64 `cstring` / `ubyte[]` arrays
4 
5     ---
6     istring blah = "Hello there, my name is Jeff.";
7     scope encodebuf = new char[allocateEncodeSize(blah.length)];
8     mstring encoded = encode(cast(const(ubyte)[])blah, encodebuf);
9 
10     scope decodebuf = new ubyte[encoded.length];
11     if (cast(cstring)decode(encoded, decodebuf) == "Hello there, my name is Jeff.")
12       Stdout("yay").newline;
13     ---
14 
15         Copyright:
16             Copyright (c) 2008 Jeff Davey.
17             Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
18             All rights reserved.
19 
20         License:
21             Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
22             See LICENSE_TANGO.txt for details.
23 
24         Standards: rfc3548, rfc2045
25 
26         Authors: Jeff Davey
27 
28 *******************************************************************************/
29 
30 module ocean.util.encode.Base64;
31 
32 import ocean.meta.types.Qualifiers;
33 import ocean.core.Verify;
34 
35 version (unittest) import ocean.core.Test;
36 
37 
38 /*******************************************************************************
39 
40     Default base64 encode/decode table
41 
42     This manifest constant is the default set of characters used by base64
43     encoding/decoding, according to RFC4648.
44 
45 *******************************************************************************/
46 
47 public static immutable istring defaultEncodeTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
48 
49 /// Ditto
50 public static immutable ubyte[char.max + 1] defaultDecodeTable = [
51     'A' :  0, 'B' :  1, 'C' :  2, 'D' :  3, 'E' :  4,
52     'F' :  5, 'G' :  6, 'H' :  7, 'I' :  8, 'J' :  9,
53     'K' : 10, 'L' : 11, 'M' : 12, 'N' : 13, 'O' : 14,
54     'P' : 15, 'Q' : 16, 'R' : 17, 'S' : 18, 'T' : 19,
55     'U' : 20, 'V' : 21, 'W' : 22, 'X' : 23, 'Y' : 24,
56     'Z' : 25,
57 
58     'a' : 26, 'b' : 27, 'c' : 28, 'd' : 29, 'e' : 30,
59     'f' : 31, 'g' : 32, 'h' : 33, 'i' : 34, 'j' : 35,
60     'k' : 36, 'l' : 37, 'm' : 38, 'n' : 39, 'o' : 40,
61     'p' : 41, 'q' : 42, 'r' : 43, 's' : 44, 't' : 45,
62     'u' : 46, 'v' : 47, 'w' : 48, 'x' : 49, 'y' : 50,
63     'z' : 51,
64 
65     '0' : 52, '1' : 53, '2' : 54, '3' : 55, '4' : 56,
66     '5' : 57, '6' : 58, '7' : 59, '8' : 60, '9' : 61,
67 
68     '+' : 62, '/' : 63,
69 
70     '=' : BASE64_PAD
71 ];
72 
73 
74 /*******************************************************************************
75 
76     URL-safe base64 encode/decode table
77 
78     This manifest constant exposes the url-safe ("base64url") variant of the
79     encode/decode table, according to RFC4648.
80 
81 *******************************************************************************/
82 
83 public static immutable istring urlSafeEncodeTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=";
84 
85 /// Ditto
86 public static immutable ubyte[char.max + 1] urlSafeDecodeTable = [
87     'A' :  0, 'B' :  1, 'C' :  2, 'D' :  3, 'E' :  4,
88     'F' :  5, 'G' :  6, 'H' :  7, 'I' :  8, 'J' :  9,
89     'K' : 10, 'L' : 11, 'M' : 12, 'N' : 13, 'O' : 14,
90     'P' : 15, 'Q' : 16, 'R' : 17, 'S' : 18, 'T' : 19,
91     'U' : 20, 'V' : 21, 'W' : 22, 'X' : 23, 'Y' : 24,
92     'Z' : 25,
93 
94     'a' : 26, 'b' : 27, 'c' : 28, 'd' : 29, 'e' : 30,
95     'f' : 31, 'g' : 32, 'h' : 33, 'i' : 34, 'j' : 35,
96     'k' : 36, 'l' : 37, 'm' : 38, 'n' : 39, 'o' : 40,
97     'p' : 41, 'q' : 42, 'r' : 43, 's' : 44, 't' : 45,
98     'u' : 46, 'v' : 47, 'w' : 48, 'x' : 49, 'y' : 50,
99     'z' : 51,
100 
101     '0' : 52, '1' : 53, '2' : 54, '3' : 55, '4' : 56,
102     '5' : 57, '6' : 58, '7' : 59, '8' : 60, '9' : 61,
103 
104     '-' : 62, '_' : 63,
105 
106     '=' : BASE64_PAD
107 ];
108 
109 
110 /// Value set to the padding
111 private static immutable ubyte BASE64_PAD = 64;
112 
113 /*******************************************************************************
114 
115     Provide the size of the data once base64 encoded
116 
117     When data is encoded in Base64, it is packed in groups of 3 bytes, which
118     are then encoded in 4 bytes (by groups of 6 bytes).
119     In case length is not a multiple of 3, we add padding.
120     It means we need `length / 3 * 4` + `length % 3 ? 4 : 0`.
121 
122     Params:
123       data = An array that will be encoded
124 
125     Returns:
126       The size needed to encode `data` in base64
127 
128 *******************************************************************************/
129 
130 
131 public size_t allocateEncodeSize (in ubyte[] data)
132 {
133     return allocateEncodeSize(data.length);
134 }
135 
136 /*******************************************************************************
137 
138     Provide the size of the data once base64 encoded
139 
140     When data is encoded in Base64, it is packed in groups of 3 bytes, which
141     are then encoded in 4 bytes (by groups of 6 bytes).
142     In case length is not a multiple of 3, we add padding.
143     It means we need `length / 3 * 4` + `length % 3 ? 4 : 0`.
144 
145     Params:
146       length = Number of bytes to be encoded
147 
148     Returns:
149       The size needed to encode a data of the provided length
150 
151 *******************************************************************************/
152 
153 public size_t allocateEncodeSize (size_t length)
154 {
155     size_t tripletCount = length / 3;
156     size_t tripletFraction = length % 3;
157     return (tripletCount + (tripletFraction ? 1 : 0)) * 4;
158 }
159 
160 
161 /*******************************************************************************
162 
163     Encodes `data` into `buff` and returns the number of bytes encoded.
164     This will not terminate and pad any "leftover" bytes, and will instead
165     only encode up to the highest number of bytes divisible by three.
166 
167     Params:
168       table = The encode table to use, either `defaultEncodeTable` (the default)
169               or `urlSafeEncodeTable`, or one's own encode table.
170       data = what is to be encoded
171       buff = buffer large enough to hold encoded data
172       bytesEncoded = ref that returns how much of the buffer was filled
173 
174     Returns:
175       The number of bytes left to encode
176 
177 *******************************************************************************/
178 
179 public int encodeChunk (istring table = defaultEncodeTable)
180     (in ubyte[] data, mstring buff, ref int bytesEncoded)
181 {
182     static assert(validateEncodeTable(table) is null,
183                   validateEncodeTable(table));
184 
185     size_t tripletCount = data.length / 3;
186     int rtn = 0;
187     char *rtnPtr = buff.ptr;
188     const(ubyte) *dataPtr = data.ptr;
189 
190     if (data.length > 0)
191     {
192         rtn = cast(int) tripletCount * 3;
193         bytesEncoded = cast(int) tripletCount * 4;
194         for (size_t i; i < tripletCount; i++)
195         {
196             *rtnPtr++ = table[((dataPtr[0] & 0xFC) >> 2)];
197             *rtnPtr++ = table[(((dataPtr[0] & 0x03) << 4) | ((dataPtr[1] & 0xF0) >> 4))];
198             *rtnPtr++ = table[(((dataPtr[1] & 0x0F) << 2) | ((dataPtr[2] & 0xC0) >> 6))];
199             *rtnPtr++ = table[(dataPtr[2] & 0x3F)];
200             dataPtr += 3;
201         }
202     }
203 
204     return rtn;
205 }
206 
207 /*******************************************************************************
208 
209     Encodes data and returns as an ASCII base64 string.
210 
211     Params:
212       table = The encode table to use, either `defaultEncodeTable` (the default)
213               or `urlSafeEncodeTable`, or one's own encode table.
214               An array of 65 chars is expected, where the last char is used
215               for padding
216       data = what is to be encoded
217       buff = buffer large enough to hold encoded data
218       pad  = Whether or not to pad the output - default to `true`
219 
220     Example:
221     ---
222     char[512] encodebuf;
223     mstring myEncodedString = encode(cast(const(ubyte)[])"Hello, how are you today?", encodebuf);
224     Stdout(myEncodedString).newline; // SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==
225     ---
226 
227 *******************************************************************************/
228 
229 public mstring encode (istring table = defaultEncodeTable)
230     (in ubyte[] data, mstring buff, bool pad = true)
231 {
232     verify(data !is null);
233     verify(buff.length >= allocateEncodeSize(data));
234 
235     static assert(validateEncodeTable(table) is null,
236                   validateEncodeTable(table));
237 
238     mstring rtn = null;
239 
240     if (data.length > 0)
241     {
242         int bytesEncoded = 0;
243         int numBytes = encodeChunk!(table)(data, buff, bytesEncoded);
244         char *rtnPtr = buff.ptr + bytesEncoded;
245         const(ubyte)* dataPtr = data.ptr + numBytes;
246         auto tripletFraction = data.length - (dataPtr - data.ptr);
247 
248         switch (tripletFraction)
249         {
250             case 2:
251                 *rtnPtr++ = table[((dataPtr[0] & 0xFC) >> 2)];
252                 *rtnPtr++ = table[(((dataPtr[0] & 0x03) << 4) | ((dataPtr[1] & 0xF0) >> 4))];
253                 *rtnPtr++ = table[((dataPtr[1] & 0x0F) << 2)];
254                 if (pad)
255                     *rtnPtr++ = table[BASE64_PAD];
256                 break;
257             case 1:
258                 *rtnPtr++ = table[((dataPtr[0] & 0xFC) >> 2)];
259                 *rtnPtr++ = table[((dataPtr[0] & 0x03) << 4)];
260                 if (pad)
261                 {
262                     *rtnPtr++ = table[BASE64_PAD];
263                     *rtnPtr++ = table[BASE64_PAD];
264                 }
265                 break;
266             default:
267                 break;
268         }
269         rtn = buff[0..(rtnPtr - buff.ptr)];
270     }
271 
272     return rtn;
273 }
274 
275 
276 /*******************************************************************************
277 
278     Encodes data and returns as an ASCII base64 string
279 
280     Params:
281       table = The encode table to use, either `defaultEncodeTable` (the default)
282               or `urlSafeEncodeTable`, or one's own encode table.
283       data = what is to be encoded
284       pad  = Whether or not to pad the output - default to `true`
285 
286     Example:
287     ---
288     mstring myEncodedString = encode(cast(ubyte[])"Hello, how are you today?");
289     Stdout(myEncodedString).newline; // SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==
290     ---
291 
292 *******************************************************************************/
293 
294 public mstring encode (istring table = defaultEncodeTable)
295     (in ubyte[] data, bool pad = true)
296 {
297     verify(data !is null);
298 
299     auto rtn = new char[allocateEncodeSize(data)];
300     return encode!(table)(data, rtn, pad);
301 }
302 
303 
304 /*******************************************************************************
305 
306     Decodes an ASCII base64 string and returns it as ubyte[] data.
307     Allocates the size of the array.
308 
309     This decoder will ignore non-base64 characters, so for example data with
310     newline in it is valid.
311     Note that the entries in the provided decode table are not required to be
312     unique. This allows the use of decode tables with alternatives for certain
313     characters.
314 
315     Since the padding is required only between multiple base64 strings (for the
316     decoder not to interpret the start of the next string as the end of the
317     previous one), data may or may not contain the trailing padding, and it will
318     still be decoded correctly.
319 
320     Note: With explicit padding, three padding bytes will cause decoder to
321     throw an Exception. However, in the case of implicit padding, the last byte
322     will be silently ignored if it contains less than two bytes. This was the
323     undocumented behaviour of the decoder, and it will be changed in the next
324     major version.
325 
326 
327     Params:
328       Table = The decode table to use. Variadic template parameter is used to
329               allow passing it as an expression in D1, but a single `ubyte[256]`
330               is expected.
331       data = what is to be decoded
332 
333     Example:
334     ---
335     mstring myDecodedString = cast(mstring)decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==");
336     Stdout(myDecodedString).newline; // Hello, how are you today?
337     ---
338 
339 *******************************************************************************/
340 
341 public ubyte[] decode () (cstring data)
342 {
343     return decode!(defaultDecodeTable)(data);
344 }
345 
346 /// Ditto
347 public ubyte[] decode (Table...) (cstring data)
348 {
349     verify(data !is null);
350 
351     auto rtn = new ubyte[data.length];
352     return decode!(Table)(data, rtn);
353 }
354 
355 /*******************************************************************************
356 
357     Decodes an ASCCI base64 string and returns it as ubyte[] data.
358 
359     This decoder will ignore non-base64 characters, so for example data with
360     newline in it is valid.
361     Note that the entries in the provided decode table are not required to be
362     unique. This allows the use of decode tables with alternatives for certain
363     characters.
364 
365     Since the padding is required only between multiple base64 strings (for the
366     decoder not to interpret the start of the next string as the end of the
367     previous one), data may or may not contain the trailing padding, and it will
368     still be decoded correctly.
369 
370     Params:
371       Table = The decode table to use. Variadic template parameter is used to
372               allow passing it as an expression in D1, but a single `ubyte[256]`
373               is expected.
374       data = what is to be decoded
375       buff = a big enough array to hold the decoded data
376 
377     Example:
378     ---
379     ubyte[512] decodebuf;
380     mstring myDecodedString = cast(mstring)decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==", decodebuf);
381     Stdout(myDecodedString).newline; // Hello, how are you today?
382     ---
383 
384 *******************************************************************************/
385 
386 public ubyte[] decode () (cstring data, ubyte[] buff)
387 {
388     return decode!(defaultDecodeTable)(data, buff);
389 }
390 
391 /// Ditto
392 public ubyte[] decode (Table...) (cstring data, ubyte[] buff)
393 {
394     verify(data !is null);
395 
396     static assert(Table.length == 1,
397                   "A single argument is expected, not: " ~ Table.stringof);
398     static assert(validateDecodeTable(Table[0]) == null,
399                   validateDecodeTable(Table[0]));
400 
401     static immutable table = Table[0];
402     ubyte[] rtn;
403 
404     if (data.length > 0)
405     {
406         ubyte[4] base64Quad;
407         ubyte *quadPtr = base64Quad.ptr;
408         ubyte *endPtr = base64Quad.ptr + 4;
409         ubyte *rtnPt = buff.ptr;
410         size_t encodedLength = 0;
411 
412         ubyte padCount = 0;
413         ubyte endCount = 0;
414         ubyte paddedPos = 0;
415         foreach_reverse(char piece; data)
416         {
417             paddedPos++;
418             ubyte current = table[piece];
419             if (current || piece == 'A')
420             {
421                 endCount++;
422                 if (current == BASE64_PAD)
423                     padCount++;
424             }
425             if (endCount == 4)
426                 break;
427         }
428 
429         if (padCount > 2)
430             throw new Exception("Improperly terminated base64 string. Base64 pad character (=) found where there shouldn't be one.");
431         if (padCount < 2)
432             paddedPos = cast(ubyte)((data.length-padCount) % 4);
433 
434         auto nonPadded = data[0..($ - paddedPos)];
435         foreach(piece; nonPadded)
436         {
437             ubyte next = table[piece];
438             if (next || piece == 'A')
439                 *quadPtr++ = next;
440             if (quadPtr is endPtr)
441             {
442                 rtnPt[0] = cast(ubyte) ((base64Quad[0] << 2) | (base64Quad[1] >> 4));
443                 rtnPt[1] = cast(ubyte) ((base64Quad[1] << 4) | (base64Quad[2] >> 2));
444                 rtnPt[2] = cast(ubyte) ((base64Quad[2] << 6) | base64Quad[3]);
445                 encodedLength += 3;
446                 quadPtr = base64Quad.ptr;
447                 rtnPt += 3;
448             }
449         }
450 
451         // this will try and decode whatever is left, even if it isn't terminated properly (ie: missing last one or two =)
452         if (paddedPos >= 2)
453         {
454             auto padded = data[($ - paddedPos) .. $];
455             verify (padded.length <= 4);
456 
457             // Fill the missing paddings if needed
458             base64Quad[padded.length .. $] = BASE64_PAD;
459 
460             foreach(i, char piece; padded)
461             {
462                 ubyte next = table[piece];
463                 if (next || piece == 'A')
464                     *quadPtr++ = next;
465                 if (i == padded.length - 1)
466                 {
467                     *rtnPt++ = cast(ubyte) (((base64Quad[0] << 2) | (base64Quad[1]) >> 4));
468                     if (base64Quad[2] != BASE64_PAD)
469                     {
470                         *rtnPt++ = cast(ubyte) (((base64Quad[1] << 4) | (base64Quad[2] >> 2)));
471                         encodedLength += 2;
472                         break;
473                     }
474                     else
475                     {
476                         encodedLength++;
477                         break;
478                     }
479                 }
480             }
481         }
482 
483         rtn = buff[0..encodedLength];
484     }
485 
486     return rtn;
487 }
488 
489 
490 unittest
491 {
492     istring str = "Hello, how are you today?";
493     const(ubyte)[] payload = cast(const(ubyte)[]) str;
494 
495     // encodeChunktest
496     {
497         mstring encoded = new char[allocateEncodeSize(payload)];
498         int bytesEncoded = 0;
499         int numBytesLeft = encodeChunk(payload, encoded, bytesEncoded);
500         cstring result = encoded[0..bytesEncoded] ~ encode(payload[numBytesLeft..$], encoded[bytesEncoded..$]);
501         test!("==")(result, "SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==");
502     }
503 
504     // encodeTest
505     {
506         mstring encoded = new char[allocateEncodeSize(payload)];
507         cstring result = encode(payload, encoded);
508         test!("==")(result, "SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==");
509 
510         cstring result2 = encode(payload);
511         test!("==")(result, "SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==");
512     }
513 
514     // decodeTest
515     {
516         ubyte[1024] decoded;
517         ubyte[] result = decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==", decoded);
518         test!("==")(result, payload);
519         result = decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw==");
520         test!("==")(result, payload);
521         result = decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw=");
522         test!("==")(result, payload);
523         result = decode("SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw");
524         test!("==")(result, payload);
525     }
526 
527     // single padding test
528     {
529         // try the string with the single `=` padding, and without it
530         auto result = cast(const(ubyte)[])"any carnal pleasure.";
531         test!("==")(result, decode("YW55IGNhcm5hbCBwbGVhc3VyZS4="));
532         test!("==")(result, decode("YW55IGNhcm5hbCBwbGVhc3VyZS4"));
533     }
534 
535     // short decode test, double-= padding
536     {
537         auto result = cast(const(ubyte)[])"M";
538 
539         test!("==")(result, decode("TQ"));
540         test!("==")(result, decode("TQ="));
541         test!("==")(result, decode("TQ=="));
542     }
543     // ditto, with a single = required for padding
544     {
545         auto result = cast(const(ubyte)[])"Ma";
546 
547         test!("==")(result, decode("TWE"));
548         test!("==")(result, decode("TWE="));
549         test!("!=")(result, decode(""));
550     }
551 
552     // in case when implicit padding is used and the last quadruple
553     // has less than two bytes needed for the decoding in which case
554     // it will be silently ignored
555     {
556         auto result = cast(const(ubyte)[])"Maq";
557         // B should be ignored since it's missing the second byte
558         test!("==")(result, decode("TWFxB"));
559     }
560 
561     // same as previous test, just there's nothing valid to be
562     // decoded in the first place
563     {
564         ubyte[] empty;
565         test!("==")(empty, decode("T"));
566     }
567 }
568 
569 // Encode without padding
570 unittest
571 {
572     istring str = "Hello, how are you today?";
573     const(ubyte)[] payload = cast(const(ubyte)[]) str;
574     cstring result = encode(payload, false);
575     test!("==")(result, "SGVsbG8sIGhvdyBhcmUgeW91IHRvZGF5Pw");
576 
577     auto decoded = cast(cstring)decode(result);
578     test!("==")(decoded, str);
579 }
580 
581 
582 /*******************************************************************************
583 
584     Helper function, called only at CTFE, to validate that the encode table
585     passed to `encode` is valid
586 
587     Params:
588       s = Input string to `encode` to check for base64 compliance
589 
590     Returns:
591       An error message if there is an error, `null` otherwise
592 
593 *******************************************************************************/
594 
595 private istring validateEncodeTable (istring s)
596 {
597     if (s.length != 65)
598         return "Base64 expects a 65-chars string for encoding, not: " ~ s;
599 
600     bool[char.max + 1] v;
601     foreach (char c; s)
602     {
603         if (v[c])
604             return "Base64 expects 65 unique chars, but '" ~  s
605                 ~ "' contains duplicated entry: " ~ c;
606         v[c] = true;
607     }
608     return null;
609 }
610 
611 unittest
612 {
613     import ocean.core.TypeConvert: assumeUnique;
614 
615     test!("is")(validateEncodeTable(defaultEncodeTable), istring.init);
616     test!("is")(validateEncodeTable(urlSafeEncodeTable), istring.init);
617     istring too_long = defaultEncodeTable ~ 'A';
618     test!("!is")(validateEncodeTable(too_long), istring.init);
619     mstring _dupes = defaultEncodeTable.dup;
620     _dupes[1] = 'A';
621     istring dupes = assumeUnique(_dupes);
622     test(dupes[0] == dupes[1]);
623     test!("!is")(validateEncodeTable(dupes), istring.init);
624 }
625 
626 
627 /*******************************************************************************
628 
629     Helper function, called only at CTFE, to validate that the decode table
630     passed to `decode` is valid
631 
632     Params:
633       table = Input encode table to `decode` to check for base64 compliance
634 
635     Returns:
636       An error message if there is an error, `null` otherwise
637 
638 *******************************************************************************/
639 
640 private istring validateDecodeTable (T) (in T table)
641 {
642     static if (!is(T : ubyte[char.max + 1]))
643     {
644         return "Expected an decode table of type `ubyte[char.max+1]`, got: "
645             ~ T.stringof;
646     }
647     else
648     {
649         // DMD BUG: Using foreach here iterates over the same index twice...
650         for (size_t i; i < table.length; ++i)
651         {
652             char decodedChar = cast(char) i;
653             ubyte encodedValue = table[i];
654 
655             if (encodedValue > BASE64_PAD)
656                 return "Decode table cannot contain values > 64";
657             // Unused entries have values 0, so that's the only one we cannot
658             // validate
659             if (encodedValue == 0)
660                 continue;
661         }
662         return null;
663     }
664 }
665 
666 unittest
667 {
668     test!("is")(validateDecodeTable(defaultDecodeTable), istring.init);
669     test!("is")(validateDecodeTable(urlSafeDecodeTable), istring.init);
670 
671     ubyte[] notATable = new ubyte[char.max + 1];
672     test!("!is")(validateDecodeTable(notATable), istring.init);
673 
674     ubyte[char.max + 1] table = defaultDecodeTable;
675     test(validateDecodeTable(table) is null);
676     table['*'] = BASE64_PAD;
677     test(table['='] == table['*']);
678     test!("is")(validateDecodeTable(table), istring.init);
679 }
680 
681 
682 // base64url tests
683 unittest
684 {
685     istring str = "Why so serious?";
686     const(ubyte)[] payload = cast(const(ubyte)[]) str;
687 
688     // encodeChunktest
689     {
690         mstring encoded = new char[allocateEncodeSize(payload)];
691         int bytesEncoded = 0;
692         int numBytesLeft = encodeChunk!(urlSafeEncodeTable)(payload, encoded, bytesEncoded);
693         cstring result = encoded[0..bytesEncoded] ~ encode!(urlSafeEncodeTable)(payload[numBytesLeft..$], encoded[bytesEncoded..$]);
694         test!("==")(result, "V2h5IHNvIHNlcmlvdXM_");
695     }
696 
697     // encodeTest
698     {
699         mstring encoded = new char[allocateEncodeSize(payload)];
700         cstring result = encode!(urlSafeEncodeTable)(payload, encoded);
701         test!("==")(result, "V2h5IHNvIHNlcmlvdXM_");
702 
703         cstring result2 = encode!(urlSafeEncodeTable)(payload);
704         test!("==")(result, "V2h5IHNvIHNlcmlvdXM_");
705     }
706 
707     // decodeTest
708     {
709         ubyte[1024] decoded;
710         ubyte[] result = decode!(urlSafeDecodeTable)("V2h5IHNvIHNlcmlvdXM_", decoded);
711         test!("==")(result, payload);
712         result = decode!(urlSafeDecodeTable)("V2h5IHNvIHNlcmlvdXM_");
713         test!("==")(result, payload);
714     }
715 }