├── .gitignore ├── Base64.lua ├── LICENSE ├── README.md ├── default.project.json ├── foreman.toml ├── selene.toml ├── wally.lock └── wally.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /roblox.toml 2 | /sourcemap.json -------------------------------------------------------------------------------- /Base64.lua: -------------------------------------------------------------------------------- 1 | --!native 2 | --!optimize 2 3 | 4 | local lookupValueToCharacter = buffer.create(64) 5 | local lookupCharacterToValue = buffer.create(256) 6 | 7 | local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 8 | local padding = string.byte("=") 9 | 10 | for index = 1, 64 do 11 | local value = index - 1 12 | local character = string.byte(alphabet, index) 13 | 14 | buffer.writeu8(lookupValueToCharacter, value, character) 15 | buffer.writeu8(lookupCharacterToValue, character, value) 16 | end 17 | 18 | local function encode(input: buffer): buffer 19 | local inputLength = buffer.len(input) 20 | local inputChunks = math.ceil(inputLength / 3) 21 | 22 | local outputLength = inputChunks * 4 23 | local output = buffer.create(outputLength) 24 | 25 | -- Since we use readu32 and chunks are 3 bytes large, we can't read the last chunk here 26 | for chunkIndex = 1, inputChunks - 1 do 27 | local inputIndex = (chunkIndex - 1) * 3 28 | local outputIndex = (chunkIndex - 1) * 4 29 | 30 | local chunk = bit32.byteswap(buffer.readu32(input, inputIndex)) 31 | 32 | -- 8 + 24 - (6 * index) 33 | local value1 = bit32.rshift(chunk, 26) 34 | local value2 = bit32.band(bit32.rshift(chunk, 20), 0b111111) 35 | local value3 = bit32.band(bit32.rshift(chunk, 14), 0b111111) 36 | local value4 = bit32.band(bit32.rshift(chunk, 8), 0b111111) 37 | 38 | buffer.writeu8(output, outputIndex, buffer.readu8(lookupValueToCharacter, value1)) 39 | buffer.writeu8(output, outputIndex + 1, buffer.readu8(lookupValueToCharacter, value2)) 40 | buffer.writeu8(output, outputIndex + 2, buffer.readu8(lookupValueToCharacter, value3)) 41 | buffer.writeu8(output, outputIndex + 3, buffer.readu8(lookupValueToCharacter, value4)) 42 | end 43 | 44 | local inputRemainder = inputLength % 3 45 | 46 | if inputRemainder == 1 then 47 | local chunk = buffer.readu8(input, inputLength - 1) 48 | 49 | local value1 = bit32.rshift(chunk, 2) 50 | local value2 = bit32.band(bit32.lshift(chunk, 4), 0b111111) 51 | 52 | buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) 53 | buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) 54 | buffer.writeu8(output, outputLength - 2, padding) 55 | buffer.writeu8(output, outputLength - 1, padding) 56 | elseif inputRemainder == 2 then 57 | local chunk = bit32.bor( 58 | bit32.lshift(buffer.readu8(input, inputLength - 2), 8), 59 | buffer.readu8(input, inputLength - 1) 60 | ) 61 | 62 | local value1 = bit32.rshift(chunk, 10) 63 | local value2 = bit32.band(bit32.rshift(chunk, 4), 0b111111) 64 | local value3 = bit32.band(bit32.lshift(chunk, 2), 0b111111) 65 | 66 | buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) 67 | buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) 68 | buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) 69 | buffer.writeu8(output, outputLength - 1, padding) 70 | elseif inputRemainder == 0 and inputLength ~= 0 then 71 | local chunk = bit32.bor( 72 | bit32.lshift(buffer.readu8(input, inputLength - 3), 16), 73 | bit32.lshift(buffer.readu8(input, inputLength - 2), 8), 74 | buffer.readu8(input, inputLength - 1) 75 | ) 76 | 77 | local value1 = bit32.rshift(chunk, 18) 78 | local value2 = bit32.band(bit32.rshift(chunk, 12), 0b111111) 79 | local value3 = bit32.band(bit32.rshift(chunk, 6), 0b111111) 80 | local value4 = bit32.band(chunk, 0b111111) 81 | 82 | buffer.writeu8(output, outputLength - 4, buffer.readu8(lookupValueToCharacter, value1)) 83 | buffer.writeu8(output, outputLength - 3, buffer.readu8(lookupValueToCharacter, value2)) 84 | buffer.writeu8(output, outputLength - 2, buffer.readu8(lookupValueToCharacter, value3)) 85 | buffer.writeu8(output, outputLength - 1, buffer.readu8(lookupValueToCharacter, value4)) 86 | end 87 | 88 | return output 89 | end 90 | 91 | local function decode(input: buffer): buffer 92 | local inputLength = buffer.len(input) 93 | local inputChunks = math.ceil(inputLength / 4) 94 | 95 | -- TODO: Support input without padding 96 | local inputPadding = 0 97 | if inputLength ~= 0 then 98 | if buffer.readu8(input, inputLength - 1) == padding then inputPadding += 1 end 99 | if buffer.readu8(input, inputLength - 2) == padding then inputPadding += 1 end 100 | end 101 | 102 | local outputLength = inputChunks * 3 - inputPadding 103 | local output = buffer.create(outputLength) 104 | 105 | for chunkIndex = 1, inputChunks - 1 do 106 | local inputIndex = (chunkIndex - 1) * 4 107 | local outputIndex = (chunkIndex - 1) * 3 108 | 109 | local value1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex)) 110 | local value2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 1)) 111 | local value3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 2)) 112 | local value4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, inputIndex + 3)) 113 | 114 | local chunk = bit32.bor( 115 | bit32.lshift(value1, 18), 116 | bit32.lshift(value2, 12), 117 | bit32.lshift(value3, 6), 118 | value4 119 | ) 120 | 121 | local character1 = bit32.rshift(chunk, 16) 122 | local character2 = bit32.band(bit32.rshift(chunk, 8), 0b11111111) 123 | local character3 = bit32.band(chunk, 0b11111111) 124 | 125 | buffer.writeu8(output, outputIndex, character1) 126 | buffer.writeu8(output, outputIndex + 1, character2) 127 | buffer.writeu8(output, outputIndex + 2, character3) 128 | end 129 | 130 | if inputLength ~= 0 then 131 | local lastInputIndex = (inputChunks - 1) * 4 132 | local lastOutputIndex = (inputChunks - 1) * 3 133 | 134 | local lastValue1 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex)) 135 | local lastValue2 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 1)) 136 | local lastValue3 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 2)) 137 | local lastValue4 = buffer.readu8(lookupCharacterToValue, buffer.readu8(input, lastInputIndex + 3)) 138 | 139 | local lastChunk = bit32.bor( 140 | bit32.lshift(lastValue1, 18), 141 | bit32.lshift(lastValue2, 12), 142 | bit32.lshift(lastValue3, 6), 143 | lastValue4 144 | ) 145 | 146 | if inputPadding <= 2 then 147 | local lastCharacter1 = bit32.rshift(lastChunk, 16) 148 | buffer.writeu8(output, lastOutputIndex, lastCharacter1) 149 | 150 | if inputPadding <= 1 then 151 | local lastCharacter2 = bit32.band(bit32.rshift(lastChunk, 8), 0b11111111) 152 | buffer.writeu8(output, lastOutputIndex + 1, lastCharacter2) 153 | 154 | if inputPadding == 0 then 155 | local lastCharacter3 = bit32.band(lastChunk, 0b11111111) 156 | buffer.writeu8(output, lastOutputIndex + 2, lastCharacter3) 157 | end 158 | end 159 | end 160 | end 161 | 162 | return output 163 | end 164 | 165 | return { 166 | encode = encode, 167 | decode = decode, 168 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Reselim 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Base64 2 | 3 | A pretty fast Luau Base64 encoder 4 | 5 | ## Installation 6 | 7 | ### [Wally](https://wally.run/) 8 | 9 | Add the following to your `wally.toml` under `[dependencies]`: 10 | 11 | ```toml 12 | base64 = "reselim/base64@3.0.0" 13 | ``` 14 | 15 | ### Manual 16 | 17 | [Download the latest release](https://github.com/Reselim/Base64/releases/latest) from the releases page and drag it into Studio. 18 | 19 | ## Usage 20 | 21 | ```lua 22 | local Base64 = require(path.to.Base64) 23 | 24 | local data = buffer.fromstring("Hello, world!") 25 | 26 | local encodedData = Base64.encode(data) -- buffer: "SGVsbG8sIHdvcmxkIQ==" 27 | local decodedData = Base64.decode(encodedData) -- buffer: "Hello, world!" 28 | 29 | print(buffer.tostring(decodedData)) -- "Hello, world!" 30 | ``` 31 | 32 | ## Benchmarks 33 | 34 | Benchmarks ran in Roblox Studio with a payload of **100,000,000** characters running on a **Ryzen 5900X** and **32GB RAM @ 3200MHz**, as of **2024/01/11** 35 | 36 | #### Native mode OFF: 37 | - Encode: 3303.27ms (30,273,037/s) 38 | - Decode: 3747.17ms (26,686,826/s) 39 | 40 | #### Native mode ON: 41 | - Encode: 461.23ms (216,813,496/s) 42 | - Decode: 596.37ms (167,680,012/s) -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base64", 3 | "tree": { 4 | "$path": "Base64.lua" 5 | } 6 | } -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | wally = { source = "UpliftGames/wally", version = "0.3.1" } -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "reselim/base64" 7 | version = "3.0.0" 8 | dependencies = [] 9 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reselim/base64" 3 | description = "A pretty fast Luau Base64 encoder" 4 | version = "3.0.0" 5 | registry = "https://github.com/UpliftGames/wally-index" 6 | realm = "shared" 7 | include = ["default.project.json", "Base64.lua"] --------------------------------------------------------------------------------