/
Base64.wurst
273 lines (219 loc) · 9.84 KB
/
Base64.wurst
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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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()