Skip to content

Commit d5c9663

Browse files
authored
Refactor base64 battery lib (#365)
Uses a different LUT method to provide a general speed up for encoding operations
1 parent 35f49c8 commit d5c9663

File tree

1 file changed

+89
-146
lines changed

1 file changed

+89
-146
lines changed

batteries/base64.luau

Lines changed: 89 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,132 @@
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-
251
--!native
262
--!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
2716

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
3021

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")
3324

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)
3726

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
4130

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
4534

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+
)
4841

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
5344

54-
local chunk = bit32.byteswap(buffer.readu32(input, inputIndex))
45+
local pair0 = BASE64_ENCODING_LUT[high]
46+
local pair1 = BASE64_ENCODING_LUT[low]
5547

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)))
6149

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
6652
end
6753

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)
9858

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))
10367

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)))
10869
end
10970

11071
return output
11172
end
11273

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")
11676

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
12688
end
12789

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
13094

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
13498

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)]
139103

140104
local chunk = bit32.bor(bit32.lshift(value1, 18), bit32.lshift(value2, 12), bit32.lshift(value3, 6), value4)
141105

142106
local character1 = bit32.rshift(chunk, 16)
143107
local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111)
144108
local character3 = bit32.band(chunk, 0b11111111)
145109

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
170114

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
174119

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)
180123
end
181124
end
182125

183126
return output
184127
end
185128

186-
return {
129+
return table.freeze({
187130
encode = encode,
188131
decode = decode,
189-
}
132+
})

0 commit comments

Comments
 (0)