├── selene.toml ├── default.project.json ├── .gitignore ├── CHANGELOG.md ├── wally.toml ├── LICENSE ├── README.md ├── tests.lua └── src └── init.lua /selene.toml: -------------------------------------------------------------------------------- 1 | std = "luau" 2 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base91", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Roblox files 2 | *.rbxm 3 | *.rbxmx 4 | 5 | # Editor configuration things 6 | .vscode/ 7 | 8 | sourcemap.json 9 | 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.1.0 4 | 5 | - Added support for `buffer` data type 6 | - Marked module with `--!native` and `--!optimize 2` 7 | - Redid documentation for module 8 | 9 | ## Version 1.0.0 10 | 11 | - Initial release 12 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dekkonot/base91" 3 | description = "Codec for converting to and from Base91" 4 | registry = "https://github.com/UpliftGames/wally-index" 5 | realm = "shared" 6 | 7 | exclude = ["selene.toml", ".gitignore", "tests.lua"] 8 | version = "1.1.0" 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Micah Reid 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 | # base91-luau 2 | 3 | An implementation of Base91 in Luau. As the primary consumer of Luau is Roblox, 4 | this module uses apostrophes (`'`) instead of quotation marks (`"`) for the 5 | output. 6 | 7 | Four functions are returned from the [main module](src/init.lua): 8 | 9 | - `encodeString` 10 | - `decodeString` 11 | - `encodeBytes` 12 | - `decodeBytes` 13 | - `encodeBuffer` 14 | - `decodeBuffer` 15 | 16 | These functions are documented below. They are also documented more thoroughly within the module itself. 17 | 18 | --- 19 | 20 | ```luau 21 | encodeString(input: string): string 22 | ``` 23 | 24 | Takes a string and applies base91 encoding to it. 25 | 26 | ```luau 27 | decodeString(input: string): string 28 | ``` 29 | 30 | Takes a base91 encoded string and decodes it. 31 | 32 | ```luau 33 | encodeBytes(input: {number}): {number} 34 | ``` 35 | 36 | Takes an array of bytes and returns them as a base91 encoded sequence. 37 | These bytes should be 8 bits. 38 | 39 | ```luau 40 | decodeBytes(input: {number}): {number} 41 | ``` 42 | 43 | Takes base91 encoded sequence of bytes and decodes them. The bytes should be 44 | 8 bits. 45 | 46 | ```luau 47 | encodeBuffer(input: buffer, skipTruncating: boolean?): buffer 48 | ``` 49 | 50 | Takes a buffer and returns a base91 encoded version of it. 51 | 52 | If `skipTruncating` is `true`, the returned buffer will be trimmed to be exactly the length of the output. The default value for `skipTruncating` is `true`. 53 | 54 | ```luau 55 | decodeBuffer(input: buffer, skipTruncating: boolean?): buffer 56 | ``` 57 | 58 | Takes a base91 encoded buffer and returns it decoded. 59 | 60 | If `skipTruncating` is `true`, the returned buffer will be trimmed to be exactly the length of the output. The default value for `skipTruncating` is `true`. 61 | -------------------------------------------------------------------------------- /tests.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | local base91 = require("src/init") 3 | 4 | local TEST_STRINGS: { { string } } = table.freeze({ 5 | { "Hello, World!", ">OwJh>}AQ;r@@Y?F" }, 6 | { "", "" }, 7 | { "f", "LB" }, 8 | { "fo", "drD" }, 9 | { "foo", "dr.J" }, 10 | { "foob", "dr/2Y" }, 11 | { "fooba", "dr/2s)A" }, 12 | { "foobar", "dr/2s)uC" }, 13 | { "A\0B", "%A]C" }, 14 | { "A\n\t\v", "=cc)C" }, 15 | { "☺☻", "A+l9tRLE" }, 16 | { 17 | -- No rights reserved whatsoever on the contents of the next string 18 | "Almost heaven, West Virginia\nBlue Ridge Mountains, Shenandoah River\nLife is old there, older than the trees\nYounger than the mountains, blowing like a breeze\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nAll my memories gather round her\nMiner's lady, stranger to blue water\nDark and dusty, painted on the sky\nMisty taste of moonshine, teardrop in my eye\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nI hear her voice, in the morning hour she calls me\nThe radio reminds me of my home far away\nAnd driving down the road I get a feeling\nThat I should have been home yesterday, yesterday\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nTake me home, down country roads\nTake me home, down country roads", 19 | "riM=Q[yCd#}uq9:mu'I80oZBHeq@]0m$'u|WGmgP>vG:1p;RqSO9vH:[pwSZ6axcLkre3@[+0lTr#x8Xii4GW_1fHe0m$@+sjlLAEefZf1RrUEv4^7jXB&=/W9*7%ztsQ_o)z`'SiSggZ5=SC4!i.hQ6PhtGf7=!e7Ra&=Evi>6YaUXtgQ*lKBJg,W!{W$4Oq]kL,X^I>>AIs!ztTDui?i/2(.3n;!+/`)Dl{UZ2N:7*8y$&9.`oja$J)vt)uCI!GvM^,g^apoM=Cu)&e#F10},kLfrLg,W.eaU!0DyWiIjeZh,DZ>AIs!ztTDui?i/2(.3n;!+/`)Dl{UZ2N:7*8y$&9.`oja$J)vi>6YaURcDm8j_k^IV/1;;RXtP^'pTBV<1]_YM%9t,^Xiy2`'UiWPgZe,%t2$60{BTj_%$J%*0emT}+eFMoDEf(func: (P...) -> R, ...: P...): (number, R) 24 | local startTime = os.clock() 25 | local result = func(...) 26 | return os.clock() - startTime, result 27 | end 28 | 29 | local ESCAPE_SEQUENCES = { 30 | ["\a"] = "\\a", 31 | ["\b"] = "\\b", 32 | ["\t"] = "\\t", 33 | ["\n"] = "\\n", 34 | ["\v"] = "\\v", 35 | ["\f"] = "\\f", 36 | ["\r"] = "\\r", 37 | } 38 | 39 | local function escapeChar(char: string): string 40 | if ESCAPE_SEQUENCES[char] then 41 | return ESCAPE_SEQUENCES[char] 42 | else 43 | local codepoint = utf8.codepoint(char) 44 | if codepoint <= 31 or codepoint >= 127 then 45 | return "\\" .. codepoint 46 | else 47 | return char 48 | end 49 | end 50 | end 51 | 52 | for n, test in TEST_STRINGS do 53 | if #test ~= 2 then 54 | error(`test #{n} had {#test} values when expecting 2`) 55 | end 56 | local inputBytes = { string.byte(test[1], 1, -1) } 57 | local outputBytes = { string.byte(test[2], 1, -1) } 58 | local inputBuffer = buffer.fromstring(test[1]) 59 | local outputBuffer = buffer.fromstring(test[2]) 60 | 61 | local bufferEncodeTime, bufferEncodeResult = time(base91.encodeBuffer, inputBuffer) 62 | local byteEncodeTime, byteEncodeResult = time(base91.encodeBytes, inputBytes) 63 | local stringEncodeTime, stringEncodeResult = time(base91.encodeString, test[1]) 64 | 65 | local bufferDecodeTime, bufferDecodeResult = time(base91.decodeBuffer, outputBuffer) 66 | local byteDecodeTime, byteDecodeResult = time(base91.decodeBytes, outputBytes) 67 | local stringDecodeTime, stringDecodeResult = time(base91.decodeString, test[2]) 68 | 69 | if stringEncodeResult == test[2] then 70 | print(`Test #{n} string encode passed -- took {stringEncodeTime}s`) 71 | else 72 | local printableResult = string.gsub(stringEncodeResult, utf8.charpattern, escapeChar) 73 | local printableExpected = string.gsub(test[2], utf8.charpattern, escapeChar) 74 | error(`Test #{n} string encode failed\nExpected:\n {printableExpected}\nGot:\n {printableResult}`) 75 | end 76 | 77 | if stringDecodeResult == test[1] then 78 | print(`Test #{n} string decode passed -- took {stringDecodeTime}s`) 79 | else 80 | local printableResult = string.gsub(stringDecodeResult, utf8.charpattern, escapeChar) 81 | local printableExpected = string.gsub(test[1], utf8.charpattern, escapeChar) 82 | error(`Test #{n} string decode failed\nExpected:\n {printableExpected}\nGot:\n {printableResult}`) 83 | end 84 | 85 | for i, enByte in byteEncodeResult do 86 | if enByte ~= outputBytes[i] then 87 | error( 88 | `Test #{n} byte encode failed -- value differed at byte {i} (expected {outputBytes[i]}, got {enByte})` 89 | ) 90 | end 91 | end 92 | print(`Test #{n} byte encode passed -- took {byteEncodeTime}s`) 93 | 94 | for i, deByte in byteDecodeResult do 95 | if deByte ~= inputBytes[i] then 96 | error(`Test #{n} byte decode failed -- value differed at byte {i} (expected {inputBytes[i]}, got {deByte})`) 97 | end 98 | end 99 | print(`Test #{n} byte encode passed -- took {byteDecodeTime}s`) 100 | 101 | if buffer.len(bufferEncodeResult) < buffer.len(outputBuffer) then 102 | error(`Test #{n} buffer encode output was smaller than expected`) 103 | end 104 | for i = 0, buffer.len(outputBuffer) - 1 do 105 | if buffer.readu8(bufferEncodeResult, i) ~= buffer.readu8(outputBuffer, i) then 106 | error( 107 | `Test #{n} buffer encode failed -- value differed at byte {i} (expected {buffer.readu8(outputBuffer, i)}, got {buffer.readu8( 108 | bufferEncodeResult, 109 | i 110 | )})` 111 | ) 112 | end 113 | end 114 | print(`Test #{n} buffer encode passed -- took {bufferEncodeTime}s`) 115 | 116 | if buffer.len(bufferDecodeResult) < buffer.len(inputBuffer) then 117 | error(`Test #{n} buffer decode output was smaller than expected`) 118 | end 119 | for i = 0, buffer.len(inputBuffer) - 1 do 120 | if buffer.readu8(bufferDecodeResult, i) ~= buffer.readu8(inputBuffer, i) then 121 | error( 122 | `Test #{n} buffer decode failed -- value differed at byte {i} (expected {buffer.readu8(inputBuffer, i)}, got {buffer.readu8( 123 | bufferDecodeResult, 124 | i 125 | )})` 126 | ) 127 | end 128 | end 129 | print(`Test #{n} buffer encode passed -- took {bufferDecodeTime}s`) 130 | 131 | print() 132 | end 133 | 134 | local input = "dekkonot" 135 | local last = input 136 | local encodeStart = os.clock() 137 | for _ = 1, 60 do 138 | last = base91.encodeString(last) 139 | end 140 | print(`Encoding stress test took {os.clock() - encodeStart}`) 141 | local decodeStart = os.clock() 142 | for _ = 1, 60 do 143 | last = base91.decodeString(last) 144 | end 145 | print(`Decoding stress test took {os.clock() - decodeStart}`) 146 | assert(input == last, "cyclical encoding followed by cyclical decoding did not result in the same value") 147 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | --!strict 2 | --!optimize 2 3 | --!native 4 | -- Pure Luau implementation of Base91 encoding and decoding. 5 | 6 | -- This is the ratio used to allocate a table for the output of the functions 7 | -- The actual ratio of expansion varies between 200% and 123% depending upon 8 | -- the size of the input. This is a nice middleground to avoid 9 | -- oversizing while still avoiding reallocations 10 | local EXPECTED_EXPANSION = 1.2308 11 | 12 | local CHAR_SET = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~']] 13 | local encodeCharSet = table.create(90) 14 | local decodeCharSet = table.create(90) 15 | 16 | for i = 1, 91 do 17 | encodeCharSet[i - 1] = string.byte(CHAR_SET, i, i) 18 | decodeCharSet[string.byte(CHAR_SET, i, i)] = i - 1 19 | end 20 | 21 | local STRING_CHUNKS = table.create(5) 22 | 23 | --- Takes an array of bytes and builds a string out of it. 24 | --- Uses `4096` byte chunks to make the string, which ends up being very fast. 25 | local function stringBuilder(input: { number }): string 26 | local inputLen = #input 27 | for i = 1, inputLen, 4096 do 28 | table.insert(STRING_CHUNKS, string.char(table.unpack(input, i, math.min(i + 4095, inputLen)))) 29 | end 30 | local output = table.concat(STRING_CHUNKS) 31 | table.clear(STRING_CHUNKS) 32 | 33 | return output 34 | end 35 | 36 | --[=[ 37 | Takes a buffer and returns its contents encoded into base91. 38 | 39 | For a function that operates on a string, see `encodeString`. 40 | For a function that operates on an array of bytes, see `encodeBytes`. 41 | 42 | @param input The buffer to encode as base91. 43 | @param skipTruncating Whether to skip trimming the buffer to be exactly the size of the output. Defaults to `false`. 44 | 45 | @return A buffer containing the contents of `input` after it has been encoded as base91. 46 | ]=] 47 | local function encodeBuffer(input: buffer, skipTruncating: boolean?): buffer 48 | local output = buffer.create(buffer.len(input) * 2) 49 | local c = 0 50 | 51 | local accum = 0 52 | local bitC = 0 53 | 54 | for i = 0, buffer.len(input) - 1 do 55 | accum = bit32.bor(accum, bit32.lshift(buffer.readu8(input, i), bitC)) 56 | bitC += 8 57 | if bitC > 13 then 58 | -- TL;DR: You can do 13 bits instead of 14 around half the time, 59 | -- which saves space at scale 60 | local codepoint = bit32.band(accum, 8191) -- 2^13 - 1 61 | if codepoint > 88 then 62 | accum = bit32.rshift(accum, 13) 63 | bitC -= 13 64 | else 65 | codepoint = bit32.band(accum, 16383) -- 2^14 - 1 66 | accum = bit32.rshift(accum, 14) 67 | bitC -= 14 68 | end 69 | -- Buffers write in little-endian, so we do this in reverse order 70 | buffer.writeu16(output, c, bit32.lshift(encodeCharSet[codepoint // 91], 8) + encodeCharSet[codepoint % 91]) 71 | c += 2 72 | end 73 | end 74 | 75 | if bitC > 0 then 76 | buffer.writeu8(output, c, encodeCharSet[accum % 91]) 77 | c += 1 78 | if bitC > 7 or accum > 90 then 79 | buffer.writeu8(output, c, encodeCharSet[accum // 91]) 80 | c += 1 81 | end 82 | end 83 | 84 | if skipTruncating then 85 | return output 86 | else 87 | local truncated = buffer.create(c) 88 | buffer.copy(truncated, 0, output, 0, c) 89 | return truncated 90 | end 91 | end 92 | 93 | --[=[ 94 | Takes a buffer and returns its contents decoded from base91 in a new 95 | buffer. 96 | 97 | For a function that operates on a string, see `decodeString`. 98 | For a function that operates on an array of bytes, see `decodeBytes`. 99 | 100 | @param input The buffer to decode from base91. 101 | @param skipTruncating Whether to skip trimming the buffer to be exactly the size of the output. Defaults to `false`. 102 | 103 | @return A buffer containing the contents of `input` that has been decoded from base91. 104 | ]=] 105 | local function decodeBuffer(input: buffer, skipTruncating: boolean?): buffer 106 | local output = buffer.create(buffer.len(input) * 2) 107 | local c = 0 108 | 109 | local accum = 0 110 | local bitC = 0 111 | local codepoint = -1 112 | 113 | -- This implementation is not my favorite thing in the world 114 | -- but it is fast enough and don't care to do it any other way 115 | for i = 0, buffer.len(input) - 1 do 116 | local byte = buffer.readu8(input, i) 117 | -- This skips things like whitespace 118 | if not decodeCharSet[byte] then 119 | continue 120 | end 121 | if codepoint == -1 then 122 | codepoint = decodeCharSet[byte] 123 | else 124 | codepoint += decodeCharSet[byte] * 91 125 | accum = bit32.bor(accum, bit32.lshift(codepoint, bitC)) 126 | if bit32.band(codepoint, 8191) > 88 then 127 | bitC += 13 128 | else 129 | bitC += 14 130 | end 131 | 132 | while bitC > 7 do 133 | buffer.writeu8(output, c, accum % 256) 134 | c += 1 135 | accum = bit32.rshift(accum, 8) 136 | bitC -= 8 137 | end 138 | codepoint = -1 139 | end 140 | end 141 | 142 | if codepoint ~= -1 then 143 | buffer.writeu8(output, c, bit32.bor(accum, bit32.lshift(codepoint, bitC)) % 256) 144 | c += 1 145 | end 146 | 147 | if skipTruncating then 148 | return output 149 | else 150 | local truncated = buffer.create(c) 151 | buffer.copy(truncated, 0, output, 0, c) 152 | return truncated 153 | end 154 | end 155 | 156 | --[=[ 157 | Takes an array of bytes and returns its contents encoded into base91. 158 | 159 | For a function that operates on a string, see `encodeString`. 160 | For a function that operates on a buffer, see `encodeBuffer`. 161 | 162 | @param input The array to encode as base91. 163 | 164 | @return An array containing the contents of `input` after it has been encoded as base91. 165 | ]=] 166 | local function encodeBytes(input: { number }): { number } 167 | local output = table.create(math.ceil(#input * EXPECTED_EXPANSION)) 168 | local c = 1 169 | 170 | local accum = 0 171 | local bitC = 0 172 | 173 | for _, byte in input do 174 | accum = bit32.bor(accum, bit32.lshift(byte, bitC)) 175 | bitC += 8 176 | if bitC > 13 then 177 | -- TL;DR: You can do 13 bits instead of 14 around half the time, 178 | -- which saves space at scale 179 | local codepoint = bit32.band(accum, 8191) -- 2^13 - 1 180 | if codepoint > 88 then 181 | accum = bit32.rshift(accum, 13) 182 | bitC -= 13 183 | else 184 | codepoint = bit32.band(accum, 16383) -- 2^14 - 1 185 | accum = bit32.rshift(accum, 14) 186 | bitC -= 14 187 | end 188 | output[c] = encodeCharSet[codepoint % 91] 189 | output[c + 1] = encodeCharSet[math.floor(codepoint / 91)] 190 | c += 2 191 | end 192 | end 193 | 194 | if bitC > 0 then 195 | output[c] = encodeCharSet[accum % 91] 196 | if bitC > 7 or accum > 90 then 197 | output[c + 1] = encodeCharSet[math.floor(accum / 91)] 198 | end 199 | end 200 | 201 | return output 202 | end 203 | 204 | --[=[ 205 | Takes an array of bytes and returns its contents decoded from base91 in a 206 | new table. 207 | 208 | For a function that operates on a string, see `decodeString`. 209 | For a function that operates on a buffer, see `decodeBuffer`. 210 | 211 | @param input The bytes to decode from base91. 212 | 213 | @return An array containing the contents of `input` that has been decoded from base91. 214 | ]=] 215 | local function decodeBytes(input: { number }): { number } 216 | local output = table.create(math.ceil(#input / EXPECTED_EXPANSION)) 217 | local c = 1 218 | 219 | local accum = 0 220 | local bitC = 0 221 | local codepoint = -1 222 | 223 | -- This implementation is not my favorite thing in the world 224 | -- but it is fast enough and don't care to do it any other way 225 | for _, byte in input do 226 | -- This skips things like whitespace 227 | if not decodeCharSet[byte] then 228 | continue 229 | end 230 | if codepoint == -1 then 231 | codepoint = decodeCharSet[byte] 232 | else 233 | codepoint += decodeCharSet[byte] * 91 234 | accum = bit32.bor(accum, bit32.lshift(codepoint, bitC)) 235 | if bit32.band(codepoint, 8191) > 88 then 236 | bitC = bitC + 13 237 | else 238 | bitC = bitC + 14 239 | end 240 | 241 | while bitC > 7 do 242 | output[c] = accum % 256 243 | c = c + 1 244 | accum = bit32.rshift(accum, 8) 245 | bitC = bitC - 8 246 | end 247 | codepoint = -1 248 | end 249 | end 250 | 251 | if codepoint ~= -1 then 252 | output[c] = bit32.bor(accum, bit32.lshift(codepoint, bitC)) % 256 253 | end 254 | 255 | return output 256 | end 257 | 258 | --[=[ 259 | Takes a string and returns its contents encoded into base91. 260 | 261 | For a function that operates on an array of bytes, see `encodeBytes`. 262 | For a function that operates on a buffer, see `encodeBuffer`. 263 | 264 | @param input The string to encode as base91. 265 | 266 | @return A string containing the contents of `input` after it has been encoded as base91. 267 | ]=] 268 | local function encodeString(input: string): string 269 | local output = table.create(#input * EXPECTED_EXPANSION) 270 | local c = 1 271 | 272 | local accum = 0 273 | local bitC = 0 274 | 275 | for i = 1, #input do 276 | accum = bit32.bor(accum, bit32.lshift(string.byte(input, i), bitC)) 277 | bitC += 8 278 | if bitC > 13 then 279 | local codepoint = bit32.band(accum, 8191) -- 2^13 - 1 280 | if codepoint > 88 then 281 | accum = bit32.rshift(accum, 13) 282 | bitC -= 13 283 | else 284 | codepoint = bit32.band(accum, 16383) -- 2^14 - 1 285 | accum = bit32.rshift(accum, 14) 286 | bitC -= 14 287 | end 288 | output[c] = encodeCharSet[codepoint % 91] 289 | output[c + 1] = encodeCharSet[math.floor(codepoint / 91)] 290 | c += 2 291 | end 292 | end 293 | 294 | if bitC > 0 then 295 | output[c] = encodeCharSet[accum % 91] 296 | if bitC > 7 or accum > 90 then 297 | output[c + 1] = encodeCharSet[math.floor(accum / 91)] 298 | end 299 | end 300 | 301 | return stringBuilder(output) 302 | end 303 | 304 | --[=[ 305 | Takes a string and returns its contents decoded from base91 as a string. 306 | 307 | For a function that operates on an array of bytes, see `decodeBytes`. 308 | For a function that operates on a buffer, see `decodeBuffer`. 309 | 310 | @param input The string to decode from base91. 311 | 312 | @return A string containing the contents of `input` that has been decoded from base91. 313 | ]=] 314 | local function decodeString(input: string): string 315 | local output = table.create(math.ceil(#input / EXPECTED_EXPANSION)) 316 | local c = 1 317 | 318 | local accum = 0 319 | local bitC = 0 320 | local codepoint = -1 321 | 322 | for i = 1, #input do 323 | local byte = string.byte(input, i) 324 | if not decodeCharSet[byte] then 325 | continue 326 | end 327 | if codepoint == -1 then 328 | codepoint = decodeCharSet[byte] 329 | else 330 | codepoint += decodeCharSet[byte] * 91 331 | accum = bit32.bor(accum, bit32.lshift(codepoint, bitC)) 332 | if bit32.band(codepoint, 8191) > 88 then 333 | bitC = bitC + 13 334 | else 335 | bitC = bitC + 14 336 | end 337 | 338 | while bitC > 7 do 339 | output[c] = accum % 256 340 | c = c + 1 341 | accum = bit32.rshift(accum, 8) 342 | bitC = bitC - 8 343 | end 344 | codepoint = -1 345 | end 346 | end 347 | 348 | if codepoint ~= -1 then 349 | output[c] = bit32.bor(accum, bit32.lshift(codepoint, bitC)) % 256 350 | end 351 | 352 | return stringBuilder(output) 353 | end 354 | 355 | return table.freeze({ 356 | encodeBuffer = encodeBuffer, 357 | decodeBuffer = decodeBuffer, 358 | 359 | encodeBytes = encodeBytes, 360 | decodeBytes = decodeBytes, 361 | 362 | encodeString = encodeString, 363 | decodeString = decodeString, 364 | }) 365 | --------------------------------------------------------------------------------