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 }