diff --git a/CLAUDE.md b/CLAUDE.md index 97ac8db..28a47c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -327,6 +327,9 @@ comparisons and nothing else decides how it reports or exits. reference protobuf implementation. - **float-vectors** — `test/float_vectors_test.lua`, differential against the C float cast and `struct.pack`. +- **lpack** — `test/lpack_test.lua`, `pb.selftest()` with `string.pack`/`unpack` + replaced by Control4's lpack shape before load, so the vendored bitn has to + decline them and pack bytes itself. Each runs once per math mode it asks for: native `math.frexp`/`math.ldexp`, and again with them cleared so the module's own fallbacks are bound. diff --git a/test/lpack_stub.lua b/test/lpack_stub.lua new file mode 100644 index 0000000..5adb9f8 --- /dev/null +++ b/test/lpack_stub.lua @@ -0,0 +1,83 @@ +--- @diagnostic disable: duplicate-set-field +-- Installs string.pack/string.unpack in the shape Control4's LuaJIT ships (lpack), +-- as measured on a controller: single-letter codes with no size suffix, '<' and +-- '=' little-endian, '>' big-endian, a NUL ends the format, a digit or unknown +-- code raises, and unpack(data, fmt, pos) returns the next position before the +-- values, with nil for a value the data cannot supply. Loaded ahead of bitn so a +-- byte helper that assumes the 5.3 dialect fails here rather than on a controller. +local SIZE = { b = 1, B = 1, c = 1, h = 2, H = 2, i = 4, I = 4, l = 4, L = 4 } + +local function pack_int(value, size, big_endian) + value = value % 2 ^ (8 * size) + local out = {} + for _ = 1, size do + out[#out + 1] = string.char(value % 256) + value = math.floor(value / 256) + end + if big_endian then + local reversed = {} + for i = size, 1, -1 do + reversed[#reversed + 1] = out[i] + end + out = reversed + end + return table.concat(out) +end + +local function unpack_int(data, pos, size, big_endian) + if #data < pos + size - 1 then + return pos, nil + end + local value = 0 + for i = 0, size - 1 do + local weight = big_endian and (size - 1 - i) or i + value = value + string.byte(data, pos + i) * 2 ^ (8 * weight) + end + return pos + size, value +end + +local function codes(fmt) + local out = {} + for c in fmt:gmatch(".") do + if c == "\0" then + break + end + out[#out + 1] = c + end + return out +end + +function string.pack(fmt, ...) + local args, i, out, big_endian = { ... }, 0, {}, false + for _, c in ipairs(codes(fmt)) do + if c == "<" or c == "=" then + big_endian = false + elseif c == ">" then + big_endian = true + elseif SIZE[c] then + i = i + 1 + out[#out + 1] = pack_int(args[i], SIZE[c], big_endian) + else + error("lpack stub: unsupported code '" .. c .. "'") + end + end + return table.concat(out) +end + +function string.unpack(data, fmt, pos) + pos = pos or 1 + local out, n, big_endian = {}, 0, false + for _, c in ipairs(codes(fmt)) do + if c == "<" or c == "=" then + big_endian = false + elseif c == ">" then + big_endian = true + elseif SIZE[c] then + n = n + 1 + pos, out[n] = unpack_int(data, pos, SIZE[c], big_endian) + else + error("lpack stub: unsupported code '" .. c .. "'") + end + end + return pos, (table.unpack or unpack)(out, 1, n) +end diff --git a/test/lpack_test.lua b/test/lpack_test.lua new file mode 100644 index 0000000..f28a28d --- /dev/null +++ b/test/lpack_test.lua @@ -0,0 +1,11 @@ +-- @test-name Selftest under lpack +-- @test-modes native +-- +-- Control4's LuaJIT has string.pack/unpack as lpack, a different dialect from 5.3. +-- The vendored bitn must decline it and take the byte-math path. + +local here = debug.getinfo(1, "S").source:match("^@(.*[/\\])") or "./" +dofile(here .. "lpack_stub.lua") + +local pb = require("protobuf") +os.exit(pb.selftest() and 0 or 1)