|
1 |
| ---[[ |
2 |
| - Pulled from https://github.com/Reselim/Base64 |
3 |
| -
|
4 |
| - Copyright (c) 2020 Reselim |
5 |
| -
|
6 |
| - Permission is hereby granted, free of charge, to any person obtaining a copy |
7 |
| - of this software and associated documentation files (the "Software"), to deal |
8 |
| - in the Software without restriction, including without limitation the rights |
9 |
| - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
10 |
| - copies of the Software, and to permit persons to whom the Software is |
11 |
| - furnished to do so, subject to the following conditions: |
12 |
| -
|
13 |
| - The above copyright notice and this permission notice shall be included in all |
14 |
| - copies or substantial portions of the Software. |
15 |
| -
|
16 |
| - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
17 |
| - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
18 |
| - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
19 |
| - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
20 |
| - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
21 |
| - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
22 |
| - SOFTWARE. |
23 |
| -]] |
24 |
| - |
25 | 1 | --!native
|
26 | 2 | --!optimize 2
|
| 3 | +--!strict |
| 4 | + |
| 5 | +local BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" |
| 6 | +local BASE64_ENCODING_LUT = table.create(4096) |
| 7 | +local BASE64_DECODING_LUT = table.create(255, 0) |
| 8 | + |
| 9 | +do |
| 10 | + for i = 0, 4095 do |
| 11 | + local hi = bit32.rshift(i, 6) + 1 |
| 12 | + local lo = bit32.band(i, 0x3F) + 1 |
| 13 | + BASE64_ENCODING_LUT[i + 1] = |
| 14 | + bit32.bor(string.byte(BASE64_ALPHABET, hi), bit32.lshift(string.byte(BASE64_ALPHABET, lo), 8)) |
| 15 | + end |
27 | 16 |
|
28 |
| -local lookupValueToCharacter = buffer.create(64) |
29 |
| -local lookupCharacterToValue = buffer.create(256) |
| 17 | + for i = 1, #BASE64_ALPHABET do |
| 18 | + BASE64_DECODING_LUT[string.byte(BASE64_ALPHABET, i)] = i - 1 |
| 19 | + end |
| 20 | +end |
30 | 21 |
|
31 |
| -local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" |
32 |
| -local padding = string.byte("=") |
| 22 | +local function encode(input_buffer: buffer): buffer |
| 23 | + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") |
33 | 24 |
|
34 |
| -for index = 1, 64 do |
35 |
| - local value = index - 1 |
36 |
| - local character = string.byte(alphabet, index) |
| 25 | + local input_length = buffer.len(input_buffer) |
37 | 26 |
|
38 |
| - buffer.writeu8(lookupValueToCharacter, value, character) |
39 |
| - buffer.writeu8(lookupCharacterToValue, character, value) |
40 |
| -end |
| 27 | + if input_length == 0 then |
| 28 | + return buffer.create(0) |
| 29 | + end |
41 | 30 |
|
42 |
| -local function encode(input: buffer): buffer |
43 |
| - local inputLength = buffer.len(input) |
44 |
| - local inputChunks = math.ceil(inputLength / 3) |
| 31 | + local output = buffer.create(((input_length + 2) // 3) * 4) |
| 32 | + local output_idx = 0 |
| 33 | + local i = 0 |
45 | 34 |
|
46 |
| - local outputLength = inputChunks * 4 |
47 |
| - local output = buffer.create(outputLength) |
| 35 | + while i + 3 <= input_length do |
| 36 | + local triple = bit32.bor( |
| 37 | + bit32.lshift(buffer.readu8(input_buffer, i), 16), |
| 38 | + bit32.lshift(buffer.readu8(input_buffer, i + 1), 8), |
| 39 | + buffer.readu8(input_buffer, i + 2) |
| 40 | + ) |
48 | 41 |
|
49 |
| - -- Since we use readu32 and chunks are 3 bytes large, we can't read the last chunk here |
50 |
| - for chunkIndex = 1, inputChunks - 1 do |
51 |
| - local inputIndex = (chunkIndex - 1) * 3 |
52 |
| - local outputIndex = (chunkIndex - 1) * 4 |
| 42 | + local high = bit32.band(bit32.rshift(triple, 12), 0xFFF) + 1 |
| 43 | + local low = bit32.band(triple, 0xFFF) + 1 |
53 | 44 |
|
54 |
| - local chunk = bit32.byteswap(buffer.readu32(input, inputIndex)) |
| 45 | + local pair0 = BASE64_ENCODING_LUT[high] |
| 46 | + local pair1 = BASE64_ENCODING_LUT[low] |
55 | 47 |
|
56 |
| - -- 8 + 24 - (6 * index) |
57 |
| - local value1 = bit32.rshift(chunk, 26) |
58 |
| - local value2 = bit32.band(bit32.rshift(chunk, 20), 0b111111) |
59 |
| - local value3 = bit32.band(bit32.rshift(chunk, 14), 0b111111) |
60 |
| - local value4 = bit32.band(bit32.rshift(chunk, 8), 0b111111) |
| 48 | + buffer.writeu32(output, output_idx, bit32.bor(pair0, bit32.lshift(pair1, 16))) |
61 | 49 |
|
62 |
| - buffer.writeu8(output, outputIndex, buffer.readu8(lookupValueToCharacter, value1)) |
63 |
| - buffer.writeu8(output, outputIndex + 1, buffer.readu8(lookupValueToCharacter, value2)) |
64 |
| - buffer.writeu8(output, outputIndex + 2, buffer.readu8(lookupValueToCharacter, value3)) |
65 |
| - buffer.writeu8(output, outputIndex + 3, buffer.readu8(lookupValueToCharacter, value4)) |
| 50 | + i += 3 |
| 51 | + output_idx += 4 |
66 | 52 | end
|
67 | 53 |
|
68 |
| - local inputRemainder = inputLength % 3 |
69 |
| - |
70 |
| - if inputRemainder == 1 then |
71 |
| - local chunk = buffer.readu8(input, inputLength - 1) |
72 |
| - |
73 |
| - local value1 = bit32.rshift(chunk, 2) |
74 |
| - local value2 = bit32.band(bit32.lshift(chunk, 4), 0b111111) |
75 |
| - |
76 |
| - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) |
77 |
| - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) |
78 |
| - buffer.writeu8(output, outputLength - 2, padding) |
79 |
| - buffer.writeu8(output, outputLength - 1, padding) |
80 |
| - elseif inputRemainder == 2 then |
81 |
| - local chunk = |
82 |
| - bit32.bor(bit32.lshift(buffer.readu8(input, inputLength - 2), 8), buffer.readu8(input, inputLength - 1)) |
83 |
| - |
84 |
| - local value1 = bit32.rshift(chunk, 10) |
85 |
| - local value2 = bit32.band(bit32.rshift(chunk, 4), 0b111111) |
86 |
| - local value3 = bit32.band(bit32.lshift(chunk, 2), 0b111111) |
87 |
| - |
88 |
| - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) |
89 |
| - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) |
90 |
| - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) |
91 |
| - buffer.writeu8(output, outputLength - 1, padding) |
92 |
| - elseif inputRemainder == 0 and inputLength ~= 0 then |
93 |
| - local chunk = bit32.bor( |
94 |
| - bit32.lshift(buffer.readu8(input, inputLength - 3), 16), |
95 |
| - bit32.lshift(buffer.readu8(input, inputLength - 2), 8), |
96 |
| - buffer.readu8(input, inputLength - 1) |
97 |
| - ) |
| 54 | + local rem = input_length - i |
| 55 | + |
| 56 | + if rem == 1 then |
| 57 | + local high = bit32.band(bit32.lshift(buffer.readu8(input_buffer, i), 4), 0xFF0) |
98 | 58 |
|
99 |
| - local value1 = bit32.rshift(chunk, 18) |
100 |
| - local value2 = bit32.band(bit32.rshift(chunk, 12), 0b111111) |
101 |
| - local value3 = bit32.band(bit32.rshift(chunk, 6), 0b111111) |
102 |
| - local value4 = bit32.band(chunk, 0b111111) |
| 59 | + local TWO_EQUALS = 0x3D3D |
| 60 | + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(TWO_EQUALS, 16))) |
| 61 | + elseif rem == 2 then |
| 62 | + local first = buffer.readu8(input_buffer, i) |
| 63 | + local second = buffer.readu8(input_buffer, i + 1) |
| 64 | + local high = bit32.bor(bit32.lshift(first, 4), bit32.rshift(second, 4)) |
| 65 | + local low_idx = bit32.lshift(bit32.band(second, 0x0F), 2) |
| 66 | + local low_equals = bit32.bor(string.byte(BASE64_ALPHABET, low_idx + 1), bit32.lshift(0x3D, 8)) |
103 | 67 |
|
104 |
| - buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) |
105 |
| - buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) |
106 |
| - buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) |
107 |
| - buffer.writeu8(output, outputLength - 1, buffer.readu8(lookupValueToCharacter, value4)) |
| 68 | + buffer.writeu32(output, output_idx, bit32.bor(BASE64_ENCODING_LUT[high + 1], bit32.lshift(low_equals, 16))) |
108 | 69 | end
|
109 | 70 |
|
110 | 71 | return output
|
111 | 72 | end
|
112 | 73 |
|
113 |
| -local function decode(input: buffer): buffer |
114 |
| - local inputLength = buffer.len(input) |
115 |
| - local inputChunks = math.ceil(inputLength / 4) |
| 74 | +local function decode(input_buffer: buffer): buffer |
| 75 | + assert(typeof(input_buffer) == "buffer", "Expected input to be a buffer") |
116 | 76 |
|
117 |
| - -- TODO: Support input without padding |
118 |
| - local inputPadding = 0 |
119 |
| - if inputLength ~= 0 then |
120 |
| - if buffer.readu8(input, inputLength - 1) == padding then |
121 |
| - inputPadding += 1 |
122 |
| - end |
123 |
| - if buffer.readu8(input, inputLength - 2) == padding then |
124 |
| - inputPadding += 1 |
125 |
| - end |
| 77 | + local input_length = buffer.len(input_buffer) |
| 78 | + |
| 79 | + if input_length == 0 then |
| 80 | + return buffer.create(0) |
| 81 | + end |
| 82 | + |
| 83 | + local padding_size = 0 |
| 84 | + if input_length >= 2 and buffer.readu16(input_buffer, input_length - 2) == 0x3D3D then |
| 85 | + padding_size = 2 |
| 86 | + elseif input_length >= 1 and buffer.readu8(input_buffer, input_length - 1) == 0x3D then |
| 87 | + padding_size = 1 |
126 | 88 | end
|
127 | 89 |
|
128 |
| - local outputLength = inputChunks * 3 - inputPadding |
129 |
| - local output = buffer.create(outputLength) |
| 90 | + -- get correct output size |
| 91 | + local output_length = ((input_length / 4) * 3) - padding_size |
| 92 | + local output = buffer.create(output_length) |
| 93 | + local chunks = input_length // 4 |
130 | 94 |
|
131 |
| - for chunkIndex = 1, inputChunks - 1 do |
132 |
| - local inputIndex = (chunkIndex - 1) * 4 |
133 |
| - local outputIndex = (chunkIndex - 1) * 3 |
| 95 | + for chunk_idx = 1, chunks do |
| 96 | + local index = (chunk_idx - 1) * 4 |
| 97 | + local out_index = (chunk_idx - 1) * 3 |
134 | 98 |
|
135 |
| - local value1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex)) |
136 |
| - local value2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 1)) |
137 |
| - local value3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 2)) |
138 |
| - local value4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 3)) |
| 99 | + local value1 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index)] |
| 100 | + local value2 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 1)] |
| 101 | + local value3 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 2)] |
| 102 | + local value4 = BASE64_DECODING_LUT[buffer.readu8(input_buffer, index + 3)] |
139 | 103 |
|
140 | 104 | local chunk = bit32.bor(bit32.lshift(value1, 18), bit32.lshift(value2, 12), bit32.lshift(value3, 6), value4)
|
141 | 105 |
|
142 | 106 | local character1 = bit32.rshift(chunk, 16)
|
143 | 107 | local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111)
|
144 | 108 | local character3 = bit32.band(chunk, 0b11111111)
|
145 | 109 |
|
146 |
| - buffer.writeu8(output, outputIndex, character1) |
147 |
| - buffer.writeu8(output, outputIndex + 1, character2) |
148 |
| - buffer.writeu8(output, outputIndex + 2, character3) |
149 |
| - end |
150 |
| - |
151 |
| - if inputLength ~= 0 then |
152 |
| - local lastInputIndex = (inputChunks - 1) * 4 |
153 |
| - local lastOutputIndex = (inputChunks - 1) * 3 |
154 |
| - |
155 |
| - local lastValue1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex)) |
156 |
| - local lastValue2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 1)) |
157 |
| - local lastValue3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 2)) |
158 |
| - local lastValue4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 3)) |
159 |
| - |
160 |
| - local lastChunk = bit32.bor( |
161 |
| - bit32.lshift(lastValue1, 18), |
162 |
| - bit32.lshift(lastValue2, 12), |
163 |
| - bit32.lshift(lastValue3, 6), |
164 |
| - lastValue4 |
165 |
| - ) |
166 |
| - |
167 |
| - if inputPadding <= 2 then |
168 |
| - local lastCharacter1 = bit32.rshift(lastChunk, 16) |
169 |
| - buffer.writeu8(output, lastOutputIndex, lastCharacter1) |
| 110 | + -- always write the first byte |
| 111 | + if out_index < output_length then |
| 112 | + buffer.writeu8(output, out_index, character1) |
| 113 | + end |
170 | 114 |
|
171 |
| - if inputPadding <= 1 then |
172 |
| - local lastCharacter2 = bit32.band(bit32.rshift(lastChunk, 8), 0b11111111) |
173 |
| - buffer.writeu8(output, lastOutputIndex + 1, lastCharacter2) |
| 115 | + -- write second byte if have space (+padding) |
| 116 | + if out_index + 1 < output_length then |
| 117 | + buffer.writeu8(output, out_index + 1, character2) |
| 118 | + end |
174 | 119 |
|
175 |
| - if inputPadding == 0 then |
176 |
| - local lastCharacter3 = bit32.band(lastChunk, 0b11111111) |
177 |
| - buffer.writeu8(output, lastOutputIndex + 2, lastCharacter3) |
178 |
| - end |
179 |
| - end |
| 120 | + -- Write third byte if we have space (+padding) |
| 121 | + if out_index + 2 < output_length then |
| 122 | + buffer.writeu8(output, out_index + 2, character3) |
180 | 123 | end
|
181 | 124 | end
|
182 | 125 |
|
183 | 126 | return output
|
184 | 127 | end
|
185 | 128 |
|
186 |
| -return { |
| 129 | +return table.freeze({ |
187 | 130 | encode = encode,
|
188 | 131 | decode = decode,
|
189 |
| -} |
| 132 | +}) |
0 commit comments