From 7102500a87676eb7222bc83f6ac88f2c34585f4c Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:10:55 -0500 Subject: [PATCH 1/6] feat: add bit32.to_signed The inverse of to_unsigned: values of 2^31 and above wrap to negative, and values already in signed range pass through. --- CLAUDE.md | 2 +- src/bitn/bit32.lua | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 81f3cbc..5fac01a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,7 @@ All three modules share a core API: `band`, `bor`, `bxor`, `bnot`, `lshift`, The surface is not uniform beyond that, and the differences bite callers: - `mask` is **bit16 and bit32 only**. `bit64` has none. -- `to_unsigned` is **bit32 only**. +- `to_unsigned` and `to_signed` are **bit32 only**. - `bit64` alone adds the Int64 constructors and accessors below, plus the compat aliases `xor`, `shr`, `lsl`, `asr`. diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index ffa8469..f4884ed 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -45,6 +45,17 @@ function bit32.to_unsigned(n) return compat_to_unsigned(n) end +--- Convert unsigned 32-bit value to signed. +--- The inverse of `to_unsigned`: values of 2^31 and above wrap to negative. +--- @param n number Unsigned 32-bit value (already-signed values pass through) +--- @return integer result Signed 32-bit value (-2^31 to 2^31 - 1) +function bit32.to_signed(n) + if n >= 0x80000000 then + return n - 0x100000000 + end + return n +end + --- Ensure value fits in 32-bit unsigned integer. --- @param n number Input value --- @return integer result 32-bit unsigned integer (0 to 0xFFFFFFFF) @@ -294,6 +305,15 @@ function bit32.selftest() { name = "to_unsigned(-2147483648)", fn = bit32.to_unsigned, inputs = { -2147483648 }, expected = 0x80000000 }, { name = "to_unsigned(-2147483647)", fn = bit32.to_unsigned, inputs = { -2147483647 }, expected = 0x80000001 }, + -- to_signed tests + { name = "to_signed(0)", fn = bit32.to_signed, inputs = { 0 }, expected = 0 }, + { name = "to_signed(1)", fn = bit32.to_signed, inputs = { 1 }, expected = 1 }, + { name = "to_signed(0x7FFFFFFF)", fn = bit32.to_signed, inputs = { 0x7FFFFFFF }, expected = 0x7FFFFFFF }, + { name = "to_signed(0x80000000)", fn = bit32.to_signed, inputs = { 0x80000000 }, expected = -2147483648 }, + { name = "to_signed(0x80000001)", fn = bit32.to_signed, inputs = { 0x80000001 }, expected = -2147483647 }, + { name = "to_signed(0xFFFFFFFF)", fn = bit32.to_signed, inputs = { 0xFFFFFFFF }, expected = -1 }, + { name = "to_signed(-1)", fn = bit32.to_signed, inputs = { -1 }, expected = -1 }, + -- band tests { name = "band(0xFF00FF00, 0x00FF00FF)", fn = bit32.band, inputs = { 0xFF00FF00, 0x00FF00FF }, expected = 0 }, { From 02e6c4c7fd467f5122ade6057f0f03ebec731e6d Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:32:15 -0500 Subject: [PATCH 2/6] perf: faster byte helpers and from_number string.pack/unpack where available, a single 8-byte string for the 64-bit encoders, and % instead of the per-bit pure Lua band. Also prints negative expected values in the bit32 selftest as decimal. --- src/bitn/bit32.lua | 28 ++++++++++++++++++++++---- src/bitn/bit64.lua | 50 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index f4884ed..953bd88 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -223,12 +223,19 @@ end local string_char = string.char local string_byte = string.byte +local string_pack = rawget(string, "pack") +local string_unpack = rawget(string, "unpack") +-- The pure Lua band is a per-bit loop, so reduce with % there instead. +local fast_band = _compat.has_native_ops or _compat.has_bit_lib --- Convert 32-bit unsigned integer to 4 bytes (big-endian). --- @param n integer 32-bit unsigned integer --- @return string bytes 4-byte string in big-endian order function bit32.u32_to_be_bytes(n) - n = compat_band(n, MASK32) + if string_pack then + return string_pack(">I4", n % 0x100000000) + end + n = fast_band and compat_band(n, MASK32) or n % 0x100000000 return string_char( math_floor(n / 16777216) % 256, math_floor(n / 65536) % 256, @@ -241,7 +248,10 @@ end --- @param n integer 32-bit unsigned integer --- @return string bytes 4-byte string in little-endian order function bit32.u32_to_le_bytes(n) - n = compat_band(n, MASK32) + if string_pack then + return string_pack("= offset + 3, "Insufficient bytes for u32") + if string_unpack then + return (string_unpack(">I4", str, offset)) + end local b1, b2, b3, b4 = string_byte(str, offset, offset + 3) return b1 * 16777216 + b2 * 65536 + b3 * 256 + b4 end @@ -268,6 +281,9 @@ end function bit32.le_bytes_to_u32(str, offset) offset = offset or 1 assert(#str >= offset + 3, "Insufficient bytes for u32") + if string_unpack then + return (string_unpack("I4I4", high, low) + end + return string_char( + math_floor(high / 16777216) % 256, + math_floor(high / 65536) % 256, + math_floor(high / 256) % 256, + math_floor(high % 256), + math_floor(low / 16777216) % 256, + math_floor(low / 65536) % 256, + math_floor(low / 256) % 256, + math_floor(low % 256) + ) end --- Convert 64-bit value to 8 bytes (little-endian). --- @param x Int64HighLow 64-bit value {high, low} --- @return string bytes 8-byte string in little-endian order function bit64.u64_to_le_bytes(x) - return bit32_u32_to_le_bytes(x[2]) .. bit32_u32_to_le_bytes(x[1]) + local high, low = x[1] % 0x100000000, x[2] % 0x100000000 + if string_pack then + return string_pack("= offset + 7, "Insufficient bytes for u64") + if string_unpack then + local high, low = string_unpack(">I4I4", str, offset) + return setmetatable({ high, low }, Int64Meta) + end local high = bit32_be_bytes_to_u32(str, offset) local low = bit32_be_bytes_to_u32(str, offset + 4) return bit64.new(high, low) @@ -300,6 +332,10 @@ end function bit64.le_bytes_to_u64(str, offset) offset = offset or 1 assert(#str >= offset + 7, "Insufficient bytes for u64") + if string_unpack then + local low, high = string_unpack(" Date: Fri, 11 Sep 2026 09:48:49 -0500 Subject: [PATCH 3/6] fix: reject offsets below 1 in the byte decoders, and pin from_number's wrap string.unpack treats offset 0 as 1, so offset 0 silently read the wrong bytes on 5.3+ while raising elsewhere. --- src/bitn/bit32.lua | 15 +++++++++++++++ src/bitn/bit64.lua | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index 953bd88..a9927bf 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -266,6 +266,7 @@ end --- @return integer n 32-bit unsigned integer function bit32.be_bytes_to_u32(str, offset) offset = offset or 1 + assert(offset >= 1, "Offset must be at least 1") assert(#str >= offset + 3, "Insufficient bytes for u32") if string_unpack then return (string_unpack(">I4", str, offset)) @@ -280,6 +281,7 @@ end --- @return integer n 32-bit unsigned integer function bit32.le_bytes_to_u32(str, offset) offset = offset or 1 + assert(offset >= 1, "Offset must be at least 1") assert(#str >= offset + 3, "Insufficient bytes for u32") if string_unpack then return (string_unpack("= 1, "Offset must be at least 1") assert(#str >= offset + 7, "Insufficient bytes for u64") if string_unpack then local high, low = string_unpack(">I4I4", str, offset) @@ -331,6 +332,7 @@ end --- @return Int64HighLow value {high, low} 64-bit value function bit64.le_bytes_to_u64(str, offset) offset = offset or 1 + assert(offset >= 1, "Offset must be at least 1") assert(#str >= offset + 7, "Insufficient bytes for u64") if string_unpack then local low, high = string_unpack(" 0 then + print(" PASS: from_number(-0.0) gives a +0 high word") + passed = passed + 1 + else + print(" FAIL: from_number(-0.0) gives a +0 high word") + end + + for _, test in ipairs({ + { name = "le_bytes_to_u64 rejects offset 0", fn = bit64.le_bytes_to_u64 }, + { name = "be_bytes_to_u64 rejects offset 0", fn = bit64.be_bytes_to_u64 }, + }) do + total = total + 1 + if not pcall(test.fn, "\1\2\3\4\5\6\7\8\9", 0) then + print(" PASS: " .. test.name) + passed = passed + 1 + else + print(" FAIL: " .. test.name) + end + end + print(string.format("\n64-bit operations: %d/%d tests passed\n", passed, total)) return passed == total end From ab64bc929e41d74febbacf12815f256032844d85 Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:05:25 -0500 Subject: [PATCH 4/6] perf: branch instead of assert in the byte decoders, and guard bit16's offsets The per-call assert made the decoders slower than v0.6.2. error() keeps the file:line prefix assert produced. --- src/bitn/bit16.lua | 27 +++++++++++++++++++++++++-- src/bitn/bit32.lua | 16 ++++++++++++---- src/bitn/bit64.lua | 16 ++++++++++++---- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/bitn/bit16.lua b/src/bitn/bit16.lua index ebb7f47..97e7e4b 100644 --- a/src/bitn/bit16.lua +++ b/src/bitn/bit16.lua @@ -171,7 +171,12 @@ end --- @return integer n 16-bit unsigned integer function bit16.be_bytes_to_u16(str, offset) offset = offset or 1 - assert(#str >= offset + 1, "Insufficient bytes for u16") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 1 then + error("Insufficient bytes for u16") + end local b1, b2 = string_byte(str, offset, offset + 1) return b1 * 256 + b2 end @@ -182,7 +187,12 @@ end --- @return integer n 16-bit unsigned integer function bit16.le_bytes_to_u16(str, offset) offset = offset or 1 - assert(#str >= offset + 1, "Insufficient bytes for u16") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 1 then + error("Insufficient bytes for u16") + end local b1, b2 = string_byte(str, offset, offset + 1) return b1 + b2 * 256 end @@ -398,6 +408,19 @@ function bit16.selftest() end end + for _, test in ipairs({ + { name = "le_bytes_to_u16 rejects offset 0", fn = bit16.le_bytes_to_u16 }, + { name = "be_bytes_to_u16 rejects offset 0", fn = bit16.be_bytes_to_u16 }, + }) do + total = total + 1 + if not pcall(test.fn, "\1\2\3", 0) then + print(" PASS: " .. test.name) + passed = passed + 1 + else + print(" FAIL: " .. test.name) + end + end + print(string.format("\n16-bit operations: %d/%d tests passed\n", passed, total)) return passed == total end diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index a9927bf..82f582f 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -266,8 +266,12 @@ end --- @return integer n 32-bit unsigned integer function bit32.be_bytes_to_u32(str, offset) offset = offset or 1 - assert(offset >= 1, "Offset must be at least 1") - assert(#str >= offset + 3, "Insufficient bytes for u32") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 3 then + error("Insufficient bytes for u32") + end if string_unpack then return (string_unpack(">I4", str, offset)) end @@ -281,8 +285,12 @@ end --- @return integer n 32-bit unsigned integer function bit32.le_bytes_to_u32(str, offset) offset = offset or 1 - assert(offset >= 1, "Offset must be at least 1") - assert(#str >= offset + 3, "Insufficient bytes for u32") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 3 then + error("Insufficient bytes for u32") + end if string_unpack then return (string_unpack("= 1, "Offset must be at least 1") - assert(#str >= offset + 7, "Insufficient bytes for u64") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 7 then + error("Insufficient bytes for u64") + end if string_unpack then local high, low = string_unpack(">I4I4", str, offset) return setmetatable({ high, low }, Int64Meta) @@ -332,8 +336,12 @@ end --- @return Int64HighLow value {high, low} 64-bit value function bit64.le_bytes_to_u64(str, offset) offset = offset or 1 - assert(offset >= 1, "Offset must be at least 1") - assert(#str >= offset + 7, "Insufficient bytes for u64") + if offset < 1 then + error("Offset must be at least 1") + end + if #str < offset + 7 then + error("Insufficient bytes for u64") + end if string_unpack then local low, high = string_unpack(" Date: Fri, 11 Sep 2026 10:13:34 -0500 Subject: [PATCH 5/6] perf: nibble tables and a mask fast path for the pure Lua band/bor/bxor The per-bit loops made every bitwise op up to 32 iterations with two floor calls each on runtimes without native or library bit ops. --- src/bitn/_compat.lua | 97 ++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/bitn/_compat.lua b/src/bitn/_compat.lua index cdbf6a4..e216d47 100644 --- a/src/bitn/_compat.lua +++ b/src/bitn/_compat.lua @@ -289,53 +289,78 @@ function _compat.impl_name() return "pure Lua" end -function _compat.band(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2 == 1) and (b % 2 == 1) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break +-- 4-bit truth tables, indexed [a * 16 + b + 1], so each op is at most 8 steps. +local AND4, OR4, XOR4 = {}, {}, {} +for a = 0, 15 do + for b = 0, 15 do + local x, y, r_and, r_or, r_xor, bit_val = a, b, 0, 0, 0, 1 + for _ = 1, 4 do + local xb, yb = x % 2, y % 2 + if xb == 1 and yb == 1 then + r_and = r_and + bit_val + end + if xb == 1 or yb == 1 then + r_or = r_or + bit_val + end + if xb ~= yb then + r_xor = r_xor + bit_val + end + x, y, bit_val = (x - xb) / 2, (y - yb) / 2, bit_val * 2 end + local i = a * 16 + b + 1 + AND4[i], OR4[i], XOR4[i] = r_and, r_or, r_xor + end +end + +-- band(a, 2^k - 1) is a % 2^k. +local LOW_MASK = {} +for k = 1, 32 do + LOW_MASK[2 ^ k - 1] = 2 ^ k +end + +function _compat.band(a, b) + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + local m = LOW_MASK[b] or LOW_MASK[a] + if m then + return (LOW_MASK[b] and a or b) % m + end + local r, scale = 0, 1 + while a > 0 and b > 0 do + local na, nb = a % 16, b % 16 + r = r + AND4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end function _compat.bor(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2 == 1) or (b % 2 == 1) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break - end + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + if a == 0 then + return b + elseif b == 0 then + return a + end + local r, scale = 0, 1 + while a > 0 or b > 0 do + local na, nb = a % 16, b % 16 + r = r + OR4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end function _compat.bxor(a, b) - local r = 0 - local bit_val = 1 - for _ = 0, 31 do - if (a % 2) ~= (b % 2) then - r = r + bit_val - end - a = math_floor(a / 2) - b = math_floor(b / 2) - bit_val = bit_val * 2 - if a == 0 and b == 0 then - break - end + a, b = math_floor(a) % 0x100000000, math_floor(b) % 0x100000000 + if a == 0 then + return b + elseif b == 0 then + return a + end + local r, scale = 0, 1 + while a > 0 or b > 0 do + local na, nb = a % 16, b % 16 + r = r + XOR4[na * 16 + nb + 1] * scale + a, b, scale = (a - na) / 16, (b - nb) / 16, scale * 16 end return r end From bf25cbc3042c3e294b66792275e431b51461ba56 Mon Sep 17 00:00:00 2001 From: Derek Miller <1340081+derek-miller@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:26:00 -0500 Subject: [PATCH 6/6] test: pin the decoder guards and the band mask fast path The offset guard checks for NaN explicitly, so a NaN offset raises the contract error on every interpreter. Vectors check the message and cover short buffers, NaN offsets and every low-bit mask width. LOW_MASK is built by doubling so 5.3+ gets integers. --- src/bitn/_compat.lua | 10 ++++++--- src/bitn/bit16.lua | 25 ++++++++++++++-------- src/bitn/bit32.lua | 41 +++++++++++++++++++++++++++--------- src/bitn/bit64.lua | 49 ++++++++++++++++++++++++++++++++++++-------- 4 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/bitn/_compat.lua b/src/bitn/_compat.lua index e216d47..bbc655a 100644 --- a/src/bitn/_compat.lua +++ b/src/bitn/_compat.lua @@ -312,10 +312,14 @@ for a = 0, 15 do end end --- band(a, 2^k - 1) is a % 2^k. +-- band(a, 2^k - 1) is a % 2^k. Built by doubling so the values are integers on 5.3+. local LOW_MASK = {} -for k = 1, 32 do - LOW_MASK[2 ^ k - 1] = 2 ^ k +do + local m = 1 + for _ = 1, 32 do + m = m * 2 + LOW_MASK[m - 1] = m + end end function _compat.band(a, b) diff --git a/src/bitn/bit16.lua b/src/bitn/bit16.lua index 97e7e4b..4dc44eb 100644 --- a/src/bitn/bit16.lua +++ b/src/bitn/bit16.lua @@ -171,7 +171,7 @@ end --- @return integer n 16-bit unsigned integer function bit16.be_bytes_to_u16(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 1 then @@ -187,7 +187,7 @@ end --- @return integer n 16-bit unsigned integer function bit16.le_bytes_to_u16(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 1 then @@ -408,16 +408,23 @@ function bit16.selftest() end end - for _, test in ipairs({ - { name = "le_bytes_to_u16 rejects offset 0", fn = bit16.le_bytes_to_u16 }, - { name = "be_bytes_to_u16 rejects offset 0", fn = bit16.be_bytes_to_u16 }, - }) do + local decoder_errors = { + { "le_bytes_to_u16 rejects offset 0", bit16.le_bytes_to_u16, "\1\2\3", 0, "Offset must be at least 1" }, + { "be_bytes_to_u16 rejects offset 0", bit16.be_bytes_to_u16, "\1\2\3", 0, "Offset must be at least 1" }, + { "le_bytes_to_u16 rejects a NaN offset", bit16.le_bytes_to_u16, "\1\2\3", 0 / 0, "Offset must be at least 1" }, + { "be_bytes_to_u16 rejects a NaN offset", bit16.be_bytes_to_u16, "\1\2\3", 0 / 0, "Offset must be at least 1" }, + { "le_bytes_to_u16 rejects a short buffer", bit16.le_bytes_to_u16, "\1", 1, "Insufficient bytes for u16" }, + { "be_bytes_to_u16 rejects a short buffer", bit16.be_bytes_to_u16, "\1\2", 2, "Insufficient bytes for u16" }, + } + for _, test in ipairs(decoder_errors) do + local test_name, fn, input, offset, message = test[1], test[2], test[3], test[4], test[5] total = total + 1 - if not pcall(test.fn, "\1\2\3", 0) then - print(" PASS: " .. test.name) + local raised, err_text = pcall(fn, input, offset) + if not raised and type(err_text) == "string" and string.find(err_text, message, 1, true) then + print(" PASS: " .. test_name) passed = passed + 1 else - print(" FAIL: " .. test.name) + print(" FAIL: " .. test_name) end end diff --git a/src/bitn/bit32.lua b/src/bitn/bit32.lua index 82f582f..ed7f89a 100644 --- a/src/bitn/bit32.lua +++ b/src/bitn/bit32.lua @@ -225,7 +225,7 @@ local string_char = string.char local string_byte = string.byte local string_pack = rawget(string, "pack") local string_unpack = rawget(string, "unpack") --- The pure Lua band is a per-bit loop, so reduce with % there instead. +-- % is the whole operation on the pure Lua backend; skip the call. local fast_band = _compat.has_native_ops or _compat.has_bit_lib --- Convert 32-bit unsigned integer to 4 bytes (big-endian). @@ -266,7 +266,7 @@ end --- @return integer n 32-bit unsigned integer function bit32.be_bytes_to_u32(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 3 then @@ -285,7 +285,7 @@ end --- @return integer n 32-bit unsigned integer function bit32.le_bytes_to_u32(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 3 then @@ -767,16 +767,37 @@ function bit32.selftest() end end - for _, test in ipairs({ - { name = "le_bytes_to_u32 rejects offset 0", fn = bit32.le_bytes_to_u32 }, - { name = "be_bytes_to_u32 rejects offset 0", fn = bit32.be_bytes_to_u32 }, - }) do + total = total + 1 + local mask_failures = {} + for k = 1, 32 do + if bit32.band(0xDEADBEEF, 2 ^ k - 1) ~= 0xDEADBEEF % 2 ^ k then + mask_failures[#mask_failures + 1] = k + end + end + if #mask_failures == 0 then + print(" PASS: band against every low-bit mask width") + passed = passed + 1 + else + print(" FAIL: band against every low-bit mask width: " .. table.concat(mask_failures, ", ")) + end + + local decoder_errors = { + { "le_bytes_to_u32 rejects offset 0", bit32.le_bytes_to_u32, "\1\2\3\4\5", 0, "Offset must be at least 1" }, + { "be_bytes_to_u32 rejects offset 0", bit32.be_bytes_to_u32, "\1\2\3\4\5", 0, "Offset must be at least 1" }, + { "le_bytes_to_u32 rejects a NaN offset", bit32.le_bytes_to_u32, "\1\2\3\4\5", 0 / 0, "Offset must be at least 1" }, + { "be_bytes_to_u32 rejects a NaN offset", bit32.be_bytes_to_u32, "\1\2\3\4\5", 0 / 0, "Offset must be at least 1" }, + { "le_bytes_to_u32 rejects a short buffer", bit32.le_bytes_to_u32, "\1\2\3", 1, "Insufficient bytes for u32" }, + { "be_bytes_to_u32 rejects a short buffer", bit32.be_bytes_to_u32, "\1\2\3\4", 2, "Insufficient bytes for u32" }, + } + for _, test in ipairs(decoder_errors) do + local test_name, fn, input, offset, message = test[1], test[2], test[3], test[4], test[5] total = total + 1 - if not pcall(test.fn, "\1\2\3\4\5", 0) then - print(" PASS: " .. test.name) + local raised, err_text = pcall(fn, input, offset) + if not raised and type(err_text) == "string" and string.find(err_text, message, 1, true) then + print(" PASS: " .. test_name) passed = passed + 1 else - print(" FAIL: " .. test.name) + print(" FAIL: " .. test_name) end end diff --git a/src/bitn/bit64.lua b/src/bitn/bit64.lua index 473edf8..cffc839 100644 --- a/src/bitn/bit64.lua +++ b/src/bitn/bit64.lua @@ -315,7 +315,7 @@ end --- @return Int64HighLow value {high, low} 64-bit value function bit64.be_bytes_to_u64(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 7 then @@ -336,7 +336,7 @@ end --- @return Int64HighLow value {high, low} 64-bit value function bit64.le_bytes_to_u64(str, offset) offset = offset or 1 - if offset < 1 then + if offset ~= offset or offset < 1 then error("Offset must be at least 1") end if #str < offset + 7 then @@ -1427,16 +1427,47 @@ function bit64.selftest() print(" FAIL: from_number(-0.0) gives a +0 high word") end - for _, test in ipairs({ - { name = "le_bytes_to_u64 rejects offset 0", fn = bit64.le_bytes_to_u64 }, - { name = "be_bytes_to_u64 rejects offset 0", fn = bit64.be_bytes_to_u64 }, - }) do + local decoder_errors = { + { "le_bytes_to_u64 rejects offset 0", bit64.le_bytes_to_u64, "\1\2\3\4\5\6\7\8\9", 0, "Offset must be at least 1" }, + { "be_bytes_to_u64 rejects offset 0", bit64.be_bytes_to_u64, "\1\2\3\4\5\6\7\8\9", 0, "Offset must be at least 1" }, + { + "le_bytes_to_u64 rejects a NaN offset", + bit64.le_bytes_to_u64, + "\1\2\3\4\5\6\7\8\9", + 0 / 0, + "Offset must be at least 1", + }, + { + "be_bytes_to_u64 rejects a NaN offset", + bit64.be_bytes_to_u64, + "\1\2\3\4\5\6\7\8\9", + 0 / 0, + "Offset must be at least 1", + }, + { + "le_bytes_to_u64 rejects a short buffer", + bit64.le_bytes_to_u64, + "\1\2\3\4\5\6\7", + 1, + "Insufficient bytes for u64", + }, + { + "be_bytes_to_u64 rejects a short buffer", + bit64.be_bytes_to_u64, + "\1\2\3\4\5\6\7\8", + 2, + "Insufficient bytes for u64", + }, + } + for _, test in ipairs(decoder_errors) do + local test_name, fn, input, offset, message = test[1], test[2], test[3], test[4], test[5] total = total + 1 - if not pcall(test.fn, "\1\2\3\4\5\6\7\8\9", 0) then - print(" PASS: " .. test.name) + local raised, err_text = pcall(fn, input, offset) + if not raised and type(err_text) == "string" and string.find(err_text, message, 1, true) then + print(" PASS: " .. test_name) passed = passed + 1 else - print(" FAIL: " .. test.name) + print(" FAIL: " .. test_name) end end