package Base64 import Bitwise import StringUtils import ByteBuffer import ChunkedString import Execute import ErrorHandling /* Base64 package provides encoding and decoding functions for the common base64 format that allows serializing and deserializing of any data by writing integers into the Encoder and reading integers from the Decoder. You'll have to implement writing/reading functions yourself. Encode Data using a builder pattern: > let encoder = new Base64Encoder() > encoder.writeByte(25) > encoder.writeInt(100500) > let data = encoder.intoData() // the encoder is consumed, no need to destroy it Decode Data using a builder pattern: > let decoder = new Base64Decoder() > while data.hasChunk() > decoder.append(data.readChunk()) > let bytes = decoder.intoData() // the decoder is consumed, no need to destroy it > let byte = bytes.readByte() > let number = bytes.readInt() This package also provides convenient methods for ChunkedStrings and ByteBuffers. Encode Data: > let data = bytes.encodeBase64() // `data` is of type `ChunkedString`, `bytes` is of type `ByteBuffer` Decode Data: > let bytes = data.decodeBase64() // `data` is of type `ChunkedString`, `bytes` is of type `ByteBuffer` */ /** Specifies how many characters to encode per a single `execute()` call. This value has been tuned to work under all optimization settings and with stacktraces included. **/ @configurable public constant ENCODES_PER_ROUND = 1000 /** Specifies how many chunks to decode per a single `execute()` call. This value has been tuned to work under all optimization settings and with stacktraces included. **/ @configurable public constant DECODES_PER_ROUND = 25 // RFC 4648 compliant Base64 charmap. constant CHARMAP = [ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P", "Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f", "g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v", "w","x","y","z","0","1","2","3","4","5","6","7","8","9","+","/" ] // RFC 4648 compliant Base64 reverse charmap. constant REVERSE_CHARMAP = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] public class Base64Encoder private ChunkedString chunkedString // We append only one char each time, and strings in WC3 get fully copied on each concatenation. // To optimize it and lower the times of copying quite big chunks, append by smaller chunks. // These chunks are stored in the `stringBuffer` before appended. private var stringBuffer = "" private var chars = 0 private static constant MAX_CHARS = 32 private var buffer = 0 private var bytes = 0 private static constant MAX_BYTES = 3 construct() this.chunkedString = new ChunkedString() construct(int maxChunkLength) this.chunkedString = new ChunkedString(maxChunkLength) ondestroy if chunkedString != null // The string is null after calling `intoData()`. destroy chunkedString // We're not using `executeWhile()` here to avoid additional overhead in this tight loop. private function writeData(ByteBuffer data) execute() -> var i = 0 while data.hasByte() and i < ENCODES_PER_ROUND writeByteUnsafe(data.readByte()) i++ if data.hasByte() writeData(data) private function flushStringBuffer() chunkedString.append(stringBuffer) stringBuffer = "" chars = 0 private function append(string char) stringBuffer += char chars++ if chars == MAX_CHARS flushStringBuffer() private function encode(int byte, int count) var remaining = byte for i = 0 to count let c = remaining.bitAnd(compiletime("11111100 00000000 00000000".fromBitString())).shiftr(18) append(CHARMAP[c]) remaining = remaining.shiftl(6) /** Writes an unsigned byte to be serialized into Base64. You must be sure that the provided integer is in the range [0, 255]. */ private function writeByteUnsafe(int n) buffer = buffer.shiftl(8) + n bytes++ if bytes == MAX_BYTES encode(buffer, 3) bytes = 0 /** Writes an unsigned short to be serialized into Base64. You must be sure that the provided integer is in the range [0, 65535]. */ private function writeShortUnsafe(int n) writeByteUnsafe(n.bitAnd(compiletime("11111111".fromBitString()))) writeByteUnsafe(n.shiftr(8)) /** Writes an unsigned byte to be serialized into Base64. */ function writeByte(int n) writeByteUnsafe(n.bitAnd(compiletime("11111111".fromBitString()))) /** Writes an unsigned short to be serialized into Base64. */ function writeShort(int n) writeShortUnsafe(n.bitAnd(compiletime("11111111 11111111".fromBitString()))) /** Writes a signed integer to be serialized into Base64. */ function writeInt(int n) writeShortUnsafe(n.bitAnd(compiletime("11111111 11111111".fromBitString()))) writeShortUnsafe(n.shiftr(16)) /** Writes all bytes from the buffer to be serialized into Base64. */ function write(ByteBuffer data) writeData(data) data.resetRead() /** Writes all bytes from the buffer to be serialized into Base64 and destroys the buffer. */ function consume(ByteBuffer data) writeData(data) destroy data /** Consumes this encoder and returns the encoded data as a ChunkedString. */ function intoData() returns ChunkedString if bytes != 0 encode(buffer.shiftl(8 * (3 - bytes)), bytes) append("=") if bytes == 1 append("=") flushStringBuffer() let tmp = chunkedString chunkedString = null destroy this return tmp public class Base64Decoder private var byteBuffer = new ByteBuffer() private var buffer = "" private var bufferLength = 0 private static constant MAX_CHARS = 4 private var lastDecoded = "" ondestroy if byteBuffer != null // The buffer is null after calling `intoData()`. destroy byteBuffer // We're not using `executeWhile()` here to avoid additional overhead in this tight loop. private function appendData(ChunkedString data) execute() -> var i = 0 while data.hasChunk() and i < DECODES_PER_ROUND append(data.readChunk()) i++ if data.hasChunk() appendData(data) /** Appends a part of the Base64-encoded data. */ function append(string data) let len = data.length() if bufferLength + len < MAX_CHARS buffer += data bufferLength += len return var i = MAX_CHARS - bufferLength writeBytes(buffer + data.substring(0, i)) while i + MAX_CHARS <= len writeBytes(data.substring(i, i + MAX_CHARS)) i += MAX_CHARS buffer = data.substring(i) bufferLength = len - i /** Appends a part of the Base64-encoded data. */ function append(ChunkedString data) appendData(data) data.resetRead() /** Appends a part of the Base64-encoded data and destroys it. */ function consume(ChunkedString data) appendData(data) destroy data @inline private static function decode(string char) returns int return REVERSE_CHARMAP[char.toChar().toInt()] private static constant DECODE_MASK = compiletime("11111111 11111111 11111111".fromBitString()) private function writeBytes(string chars) lastDecoded = chars var data = decode(chars.charAt(0)).shiftl(18) + decode(chars.charAt(1)).shiftl(12) + decode(chars.charAt(2)).shiftl(6) + decode(chars.charAt(3)) byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16)) data = data.shiftl(8) byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16)) data = data.shiftl(8) byteBuffer.writeByte(data.bitAnd(DECODE_MASK).shiftr(16)) /** Consumes this decoder and returns the decoded data as a ByteBuffer. **/ function intoData() returns ByteBuffer if bufferLength > 0 error("Base64 ERROR: The Base-64 encoded data should have length divisible by 4.") byteBuffer.truncate(byteBuffer.size() - lastDecoded.countOccurences("=")) let tmp = byteBuffer byteBuffer = null destroy this return tmp /** Encodes the bytes in this buffer to a string according to the Base64 format. */ public function ByteBuffer.encodeBase64() returns ChunkedString return new Base64Encoder()..consume(this).intoData() /** Decodes the bytes encoded into this string according to the Base64 format. */ public function ChunkedString.decodeBase64() returns ByteBuffer return new Base64Decoder()..consume(this).intoData() /** Decodes the bytes encoded into this string according to the Base64 format. */ public function string.decodeBase64() returns ByteBuffer return new Base64Decoder()..append(this).intoData()