diff --git a/run_tests.sh b/run_tests.sh index 9a6a0d6..e7faf08 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -46,8 +46,8 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) lua_path="$script_dir/?.lua;$script_dir/?/init.lua;$script_dir/src/?.lua;$script_dir/src/?/init.lua;$script_dir/vendor/?.lua;$LUA_PATH" # Parse command line arguments to determine which modules to run -default_modules=("bit16" "bit32" "bit64") -all_modules=("bit16" "bit32" "bit64") +default_modules=("bit16" "bit32" "bit64" "probe" "lpack") +all_modules=("bit16" "bit32" "bit64" "probe" "lpack") modules_to_run=("$@") # Validate modules if specified @@ -131,6 +131,32 @@ run_selftest "16-bit operations" "bit16" "bitn.bit16" run_selftest "32-bit operations" "bit32" "bitn.bit32" run_selftest "64-bit operations" "bit64" "bitn.bit64" +# The probe must bind a real 5.3-dialect string.pack, or the byte helpers silently +# lose their fast path; only the 5.3+ legs can prove this. +run_test "string.pack binds when it is the 5.3 dialect" "probe" " + local _compat = require('bitn._compat') + if rawget(string, 'pack') and _compat.string_pack == nil then + print('string.pack is present but the probe declined it') + os.exit(1) + end + print(_compat.string_pack and 'string.pack bound' or 'no string.pack on this interpreter') + " + +# Control4's LuaJIT has string.pack/unpack as lpack, a different dialect: every +# selftest again with that shape installed first, and the 5.3 fast path must stay unbound. +run_test "All operations with lpack-shaped string.pack" "lpack" " + dofile('$script_dir/test/lpack_stub.lua') + if require('bitn._compat').string_pack ~= nil then + print('lpack-shaped string.pack was bound as the 5.3 dialect') + os.exit(1) + end + for _, name in ipairs({ 'bitn.bit16', 'bitn.bit32', 'bitn.bit64' }) do + if not require(name).selftest() then + os.exit(1) + end + end + " + passed_count=${#passed_modules[@]} failed_count=${#failed_modules[@]} total_count=$((passed_count + failed_count)) diff --git a/src/bitn/_compat.lua b/src/bitn/_compat.lua index bbc655a..fbeebc1 100644 --- a/src/bitn/_compat.lua +++ b/src/bitn/_compat.lua @@ -29,6 +29,31 @@ _compat.to_unsigned = to_unsigned -- Constants local MASK32 = 0xFFFFFFFF +-- string.pack/unpack are bound only if they answer in the 5.3 dialect. Control4's +-- LuaJIT ships lpack under the same names: no size suffixes, and unpack takes +-- (data, fmt, pos) and returns the position first, so existence proves nothing. +local function probe_string_pack() + local pack, unpack = rawget(string, "pack"), rawget(string, "unpack") + if not (pack and unpack) then + return nil, nil + end + local packed_ok, packed = pcall(pack, "I4", 0x04030201) + if not packed_ok or packed ~= "\4\3\2\1" then + return nil, nil + end + local unpacked_ok, value, next_pos = pcall(unpack, "' 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