From f1656d4e23525c22f298e38999cbb36a82c07a75 Mon Sep 17 00:00:00 2001 From: TheHomecraft Date: Fri, 12 Jun 2026 20:18:20 +0200 Subject: [PATCH] Add lib/string_pack.lua --- lib/string_pack.lua | 562 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 562 insertions(+) create mode 100644 lib/string_pack.lua diff --git a/lib/string_pack.lua b/lib/string_pack.lua new file mode 100644 index 0000000..d29839a --- /dev/null +++ b/lib/string_pack.lua @@ -0,0 +1,562 @@ +-- MIT License +-- +-- Copyright (c) 2021 JackMacWindows +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. + +-- If not using CC, replace `expect` with a suitable argument checking function. +--local expect = require "cc.expect".expect +local expect = dofile "/rom/modules/main/cc/expect.lua".expect + +local ByteOrder = {BIG_ENDIAN = 1, LITTLE_ENDIAN = 2} +local isint = {b = 1, B = 1, h = 1, H = 1, l = 1, L = 1, j = 1, J = 1, T = 1} +local packoptsize_tbl = {b = 1, B = 1, x = 1, h = 2, H = 2, f = 4, j = 4, J = 4, l = 8, L = 8, T = 8, d = 8, n = 8} + +local function round(n) if n % 1 >= 0.5 then return math.ceil(n) else return math.floor(n) end end + +local function floatToRawIntBits(f) + if f == 0 then return 0 + elseif f == -0 then return 0x80000000 + elseif f == math.huge then return 0x7F800000 + elseif f == -math.huge then return 0xFF800000 end + local m, e = math.frexp(f) + if e > 127 or e < -126 then error("number out of range", 3) end + e, m = e + 126, round((math.abs(m) - 0.5) * 0x1000000) + if m > 0x7FFFFF then e = e + 1 end + return bit32.bor(f < 0 and 0x80000000 or 0, bit32.lshift(bit32.band(e, 0xFF), 23), bit32.band(m, 0x7FFFFF)) +end + +local function doubleToRawLongBits(f) + if f == 0 then return 0, 0 + elseif f == -0 then return 0x80000000, 0 + elseif f == math.huge then return 0x7FF00000, 0 + elseif f == -math.huge then return 0xFFF00000, 0 end + local m, e = math.frexp(f) + if e > 1023 or e < -1022 then error("number out of range", 3) end + e, m = e + 1022, round((math.abs(m) - 0.5) * 0x20000000000000) + if m > 0xFFFFFFFFFFFFF then e = e + 1 end + return bit32.bor(f < 0 and 0x80000000 or 0, bit32.lshift(bit32.band(e, 0x7FF), 20), bit32.band(m / 0x100000000, 0xFFFFF)), bit32.band(m, 0xFFFFFFFF) +end + +local function intBitsToFloat(l) + if l == 0 then return 0 + elseif l == 0x80000000 then return -0 + elseif l == 0x7F800000 then return math.huge + elseif l == 0xFF800000 then return -math.huge end + local m, e = bit32.band(l, 0x7FFFFF), bit32.band(bit32.rshift(l, 23), 0xFF) + e, m = e - 126, m / 0x1000000 + 0.5 + local n = math.ldexp(m, e) + return bit32.btest(l, 0x80000000) and -n or n +end + +local function longBitsToDouble(lh, ll) + if lh == 0 and ll == 0 then return 0 + elseif lh == 0x80000000 and ll == 0 then return -0 + elseif lh == 0x7FF00000 and ll == 0 then return math.huge + elseif lh == 0xFFF00000 and ll == 0 then return -math.huge end + local m, e = bit32.band(lh, 0xFFFFF) * 0x100000000 + bit32.band(ll, 0xFFFFFFFF), bit32.band(bit32.rshift(lh, 20), 0x7FF) + e, m = e - 1022, m / 0x20000000000000 + 0.5 + local n = math.ldexp(m, e) + return bit32.btest(lh, 0x80000000) and -n or n +end + +local function packint(num, size, output, offset, alignment, endianness, signed) + local total_size = 0 + if offset % math.min(size, alignment) ~= 0 and alignment > 1 then + local i = 0 + while offset % math.min(size, alignment) ~= 0 and i < alignment do + output[offset] = 0 + offset = offset + 1 + total_size = total_size + 1 + i = i + 1 + end + end + if endianness == ByteOrder.BIG_ENDIAN then + local added_padding = 0 + if size > 8 then for i = 0, size - 9 do + output[offset + i] = (signed and num >= 2^(size * 8 - 1) ~= 0) and 0xFF or 0 + added_padding = added_padding + 1 + total_size = total_size + 1 + end end + for i = added_padding, size - 1 do + output[offset + i] = bit32.band(bit32.rshift(num, ((size - i - 1) * 8)), 0xFF) + total_size = total_size + 1 + end + else + for i = 0, math.min(size, 8) - 1 do + output[offset + i] = num / 2^(i * 8) % 256 + total_size = total_size + 1 + end + for i = 8, size - 1 do + output[offset + i] = (signed and num >= 2^(size * 8 - 1) ~= 0) and 0xFF or 0 + total_size = total_size + 1 + end + end + return total_size +end + +local function unpackint(str, offset, size, endianness, alignment, signed) + local result, rsize = 0, 0 + if offset % math.min(size, alignment) ~= 0 and alignment > 1 then + for i = 0, alignment - 1 do + if offset % math.min(size, alignment) == 0 then break end + offset = offset + 1 + rsize = rsize + 1 + end + end + for i = 0, size - 1 do + result = result + str:byte(offset + i) * 2^((endianness == ByteOrder.BIG_ENDIAN and size - i - 1 or i) * 8) + rsize = rsize + 1 + end + if (signed and result >= 2^(size * 8 - 1)) then result = result - 2^(size * 8) end + return result, rsize +end + +local function packoptsize(opt, alignment) + local retval = packoptsize_tbl[opt] or 0 + if (alignment > 1 and retval % alignment ~= 0) then retval = retval + (alignment - (retval % alignment)) end + return retval +end + +--[[ + * string.pack (fmt, v1, v2, ...) + * + * Returns a binary string containing the values v1, v2, etc. + * serialized in binary form (packed) according to the format string fmt. + ]] +local function pack(...) + local fmt = expect(1, ..., "string") + local endianness = ByteOrder.LITTLE_ENDIAN + local alignment = 1 + local pos = 1 + local argnum = 2 + local output = {} + local i = 1 + while i <= #fmt do + local c = fmt:sub(i, i) + i = i + 1 + if c == '=' or c == '<' then + endianness = ByteOrder.LITTLE_ENDIAN + elseif c == '>' then + endianness = ByteOrder.BIG_ENDIAN + elseif c == '!' then + local size = -1 + while (i <= #fmt and fmt:sub(i, i):match("%d")) do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (math.max(size, 0) * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16 or size == 0) then error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size == -1) then alignment = 4 + else alignment = size end + elseif isint[c] then + local num = expect(argnum, select(argnum, ...), "number") + argnum = argnum + 1 + if (num >= math.pow(2, (packoptsize(c, 0) * 8 - (c:match("%l") and 1 or 0))) or + num < (c:match("%l") and -math.pow(2, (packoptsize(c, 0) * 8 - 1)) or 0)) then + error(string.format("bad argument #%d to 'pack' (integer overflow)", argnum - 1), 2) + end + pos = pos + packint(num, packoptsize(c, 0), output, pos, alignment, endianness, false) + elseif c:lower() == 'i' then + local signed = c == 'i' + local size = -1 + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (math.max(size, 0) * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16 or size == 0) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (alignment > 1 and (size ~= 1 and size ~= 2 and size ~= 4 and size ~= 8 and size ~= 16)) then + error("bad argument #1 to 'pack' (format asks for alignment not power of 2)", 2) + elseif (size == -1) then size = 4 end + local num = expect(argnum, select(argnum, ...), "number") + argnum = argnum + 1 + if (num >= math.pow(2, (size * 8 - (c:match("%l") and 1 or 0))) or + num < (c:match("%l") and -math.pow(2, (size * 8 - 1)) or 0)) then + error(string.format("bad argument #%d to 'pack' (integer overflow)", argnum - 1), 2) + end + pos = pos + packint(num, size, output, pos, alignment, endianness, signed) + elseif c == 'f' then + local f = expect(argnum, select(argnum, ...), "number") + argnum = argnum + 1 + local l = floatToRawIntBits(f) + if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then + for j = 0, alignment - 1 do + if pos % math.min(4, alignment) == 0 then break end + output[pos] = 0 + pos = pos + 1 + end + end + for j = 0, 3 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 3 - j or j)] = bit32.band(bit32.rshift(l, (j * 8)), 0xFF) end + pos = pos + 4 + elseif c == 'd' or c == 'n' then + local f = expect(argnum, select(argnum, ...), "number") + argnum = argnum + 1 + local lh, ll = doubleToRawLongBits(f) + if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then + for j = 0, alignment - 1 do + if pos % math.min(8, alignment) == 0 then break end + output[pos] = 0 + pos = pos + 1 + end + end + for j = 0, 3 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 7 - j or j)] = bit32.band(bit32.rshift(ll, (j * 8)), 0xFF) end + for j = 4, 7 do output[pos + (endianness == ByteOrder.BIG_ENDIAN and 7 - j or j)] = bit32.band(bit32.rshift(lh, ((j - 4) * 8)), 0xFF) end + pos = pos + 8 + elseif c == 'c' then + local size = 0 + if (i > #fmt or not fmt:sub(i, i):match("%d")) then + error("missing size for format option 'c'", 2) + end + while (i <= #fmt and fmt:sub(i, i):match("%d")) do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (pos + size < pos or pos + size > 0xFFFFFFFF) then error("bad argument #1 to 'pack' (format result too large)", 2) end + local str = expect(argnum, select(argnum, ...), "string") + argnum = argnum + 1 + if (#str > size) then error(string.format("bad argument #%d to 'pack' (string longer than given size)", argnum - 1), 2) end + if size > 0 then + for j = 0, size - 1 do output[pos+j] = str:byte(j + 1) or 0 end + pos = pos + size + end + elseif c == 'z' then + local str = expect(argnum, select(argnum, ...), "string") + argnum = argnum + 1 + for b in str:gmatch "." do if (b == '\0') then error(string.format("bad argument #%d to 'pack' (string contains zeros)", argnum - 1), 2) end end + for j = 0, #str - 1 do output[pos+j] = str:byte(j + 1) end + output[pos + #str] = 0 + pos = pos + #str + 1 + elseif c == 's' then + local size = 0 + while (i <= #fmt and fmt:sub(i, i):match("%d")) do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size == 0) then size = 4 end + local str = expect(argnum, select(argnum, ...), "string") + argnum = argnum + 1 + if (#str >= math.pow(2, (size * 8))) then + error(string.format("bad argument #%d to 'pack' (string length does not fit in given size)", argnum - 1), 2) + end + packint(#str, size, output, pos, 1, endianness, false) + for j = size, #str + size - 1 do output[pos+j] = str:byte(j - size + 1) or 0 end + pos = pos + #str + size + elseif c == 'x' then + output[pos] = 0 + pos = pos + 1 + elseif c == 'X' then + if (i >= #fmt) then error("invalid next option for option 'X'", 2) end + local size = 0 + local c = fmt:sub(i, i) + i = i + 1 + if c:lower() == 'i' then + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16 or size == 0) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + end + else size = packoptsize(c, 0) end + if (size < 1) then error("invalid next option for option 'X'", 2) end + if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(size, alignment) == 0 then break end + output[pos] = 0 + pos = pos + 1 + end + end + elseif c ~= ' ' then error(string.format("invalid format option '%s'", c), 2) end + end + return string.char(table.unpack(output)) +end + +--[[ + * string.packsize (fmt) + * + * Returns the size of a string resulting from string.pack with the given format. + * The format string cannot have the variable-length options 's' or 'z'. + ]] +local function packsize(fmt) + local pos = 0 + local alignment = 1 + local i = 1 + while i <= #fmt do + local c = fmt:sub(i, i) + i = i + 1 + if c == '!' then + local size = 0 + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size == 0) then alignment = 4 + else alignment = size end + elseif isint[c] then + local size = packoptsize(c, 0) + if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(size, alignment) == 0 then break end + pos = pos + 1 + end + end + pos = pos + size + elseif c:lower() == 'i' then + local size = 0 + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then + error(string.format("integral size (%d) out of limits [1,16]", size)) + elseif (alignment > 1 and (size ~= 1 and size ~= 2 and size ~= 4 and size ~= 8 and size ~= 16)) then + error("bad argument #1 to 'pack' (format asks for alignment not power of 2)", 2) + elseif (size == 0) then size = 4 end + if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(size, alignment) == 0 then break end + pos = pos + 1 + end + end + pos = pos + size + elseif c == 'f' then + if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(4, alignment) == 0 then break end + pos = pos + 1 + end + end + pos = pos + 4 + elseif c == 'd' or c == 'n' then + if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(8, alignment) == 0 then break end + pos = pos + 1 + end + end + pos = pos + 8 + elseif c == 'c' then + local size = 0 + if (i > #fmt or not fmt:sub(i, i):match("%d")) then + error("missing size for format option 'c'", 2) + end + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (pos + size < pos or pos + size > 0x7FFFFFFF) then error("bad argument #1 to 'packsize' (format result too large)", 2) end + pos = pos + size + elseif c == 'x' then + pos = pos + 1 + elseif c == 'X' then + if (i >= #fmt) then error("invalid next option for option 'X'", 2) end + local size = 0 + local c = fmt:sub(i, i) + i = i + 1 + if c:lower() == 'i' then + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16 or size == 0) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + end + else size = packoptsize(c, 0) end + if (size < 1) then error("invalid next option for option 'X'", 2) end + if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(size, alignment) == 0 then break end + pos = pos + 1 + end + end + elseif c == 's' or c == 'z' then error("bad argument #1 to 'packsize' (variable-length format)", 2) + elseif c ~= ' ' and c ~= '<' and c ~= '>' and c ~= '=' then error(string.format("invalid format option '%s'", c), 2) end + end + return pos +end + +--[[ + * string.unpack (fmt, s [, pos]) + * + * Returns the values packed in string s (see string.pack) according to the format string fmt. + * An optional pos marks where to start reading in s (default is 1). + * After the read values, this function also returns the index of the first unread byte in s. + ]] +local function unpack(fmt, str, pos) + expect(1, fmt, "string") + expect(2, str, "string") + expect(3, pos, "number", "nil") + if pos then + if (pos < 0) then pos = #str + pos + elseif (pos == 0) then error("bad argument #3 to 'unpack' (initial position out of string)", 2) end + if (pos > #str or pos < 0) then error("bad argument #3 to 'unpack' (initial position out of string)", 2) end + else pos = 1 end + local endianness = ByteOrder.LITTLE_ENDIAN + local alignment = 1 + local retval = {} + local i = 1 + while i <= #fmt do + local c = fmt:sub(i, i) + i = i + 1 + if c == '<' or c == '=' then + endianness = ByteOrder.LITTLE_ENDIAN + elseif c == '>' then + endianness = ByteOrder.BIG_ENDIAN + elseif c == '!' then + local size = 0 + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then + error(string.format("integral size (%d) out of limits [1,16]", size)) + elseif (size == 0) then alignment = 4 + else alignment = size end + elseif isint[c] then + if (pos + packoptsize(c, 0) > #str + 1) then error("data string too short", 2) end + local res, ressz = unpackint(str, pos, packoptsize(c, 0), endianness, alignment, c:match("%l") ~= nil) + retval[#retval+1] = res + pos = pos + ressz + elseif c:lower() == 'i' then + local signed = c == 'i' + local size = 0 + while (i <= #fmt and fmt:sub(i, i):match("%d")) do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size > 8) then + error(string.format("%d-byte integer does not fit into Lua Integer", size), 2) + elseif (size == 0) then size = 4 end + if (pos + size > #str + 1) then error("data string too short", 2) end + local res, ressz = unpackint(str, pos, size, endianness, alignment, signed) + retval[#retval+1] = res + pos = pos + ressz + elseif c == 'f' then + if (pos % math.min(4, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(4, alignment) == 0 then break end + pos = pos + 1 + end + end + if (pos + 4 > #str + 1) then error("data string too short", 2) end + local res = unpackint(str, pos, 4, endianness, alignment, false) + retval[#retval+1] = intBitsToFloat(res) + pos = pos + 4 + elseif c == 'd' or c == 'n' then + if (pos % math.min(8, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(8, alignment) == 0 then break end + pos = pos + 1 + end + end + if (pos + 8 > #str + 1) then error("data string too short", 2) end + local lh, ll = 0, 0 + for j = 0, 3 do lh = bit32.bor(lh, bit32.lshift((str:byte(pos + j)), ((endianness == ByteOrder.BIG_ENDIAN and 3 - j or j) * 8))) end + for j = 0, 3 do ll = bit32.bor(ll, bit32.lshift((str:byte(pos + j + 4)), ((endianness == ByteOrder.BIG_ENDIAN and 3 - j or j) * 8))) end + if endianness == ByteOrder.LITTLE_ENDIAN then lh, ll = ll, lh end + retval[#retval+1] = longBitsToDouble(lh, ll) + pos = pos + 8 + elseif c == 'c' then + local size = 0 + if (i > #fmt or not fmt:sub(i, i):match("%d")) then + error("missing size for format option 'c'", 2) + end + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)") end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (pos + size > #str + 1) then error("data string too short", 2) end + retval[#retval+1] = str:sub(pos, pos + size - 1) + pos = pos + size + elseif c == 'z' then + local size = 0 + while (str:byte(pos + size) ~= 0) do + size = size + 1 + if (pos + size > #str) then error("unfinished string for format 'z'", 2) end + end + retval[#retval+1] = str:sub(pos, pos + size - 1) + pos = pos + size + 1 + elseif c == 's' then + local size = 0 + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size == 0) then size = 4 end + if (pos + size > #str + 1) then error("data string too short", 2) end + local num, numsz = unpackint(str, pos, size, endianness, alignment, false) + pos = pos + numsz + if (pos + num > #str + 1) then error("data string too short", 2) end + retval[#retval+1] = str:sub(pos, pos + num - 1) + pos = pos + num + elseif c == 'x' then + pos = pos + 1 + elseif c == 'X' then + if (i >= #fmt) then error("invalid next option for option 'X'", 2) end + local size = 0 + local c = fmt:sub(i, i) + i = i + 1 + if c:lower() == 'i' then + while i <= #fmt and fmt:sub(i, i):match("%d") do + if (size >= 0xFFFFFFFF / 10) then error("bad argument #1 to 'pack' (invalid format)", 2) end + size = (size * 10) + tonumber(fmt:sub(i, i)) + i = i + 1 + end + if (size > 16 or size == 0) then + error(string.format("integral size (%d) out of limits [1,16]", size), 2) + elseif (size == -1) then size = 4 end + else size = packoptsize(c, 0) end + if (size < 1) then error("invalid next option for option 'X'", 2) end + if (pos % math.min(size, alignment) ~= 0 and alignment > 1) then + for j = 1, alignment do + if pos % math.min(size, alignment) == 0 then break end + pos = pos + 1 + end + end + elseif c ~= ' ' then error(string.format("invalid format option '%s'", c), 2) end + end + retval[#retval+1] = pos + return table.unpack(retval) +end + +return { + pack = pack, + packsize = packsize, + unpack = unpack +} \ No newline at end of file