diff --git a/Modules/Libs/QuestieLoader.lua b/Modules/Libs/QuestieLoader.lua index a34e59e..ec5e0df 100644 --- a/Modules/Libs/QuestieLoader.lua +++ b/Modules/Libs/QuestieLoader.lua @@ -19,6 +19,111 @@ if not math.mod then end end +-- Shim for Lua 5.0 where the bit library may be missing. +-- Questie uses band/bor/bxor/lshift/rshift in core runtime code, so we provide +-- a pure-Lua fallback when the host does not expose one. +if not bit or not bit.band or not bit.bor or not bit.bxor or not bit.lshift or not bit.rshift then + local bitlib = bit or {} + local U32 = 4294967296 + local U32_MAX = 4294967295 + local floor = math.floor + local mod = math.mod or function(a, b) + return a - floor(a / b) * b + end + + local function normalizeU32(n) + n = tonumber(n) or 0 + n = floor(n) + if n < 0 then + n = U32 + mod(n, U32) + elseif n >= U32 then + n = mod(n, U32) + end + return n + end + + local function bitAt(n, mask) + return mod(floor(n / mask), 2) + end + + local function band32(a, b) + a = normalizeU32(a) + b = normalizeU32(b) + local result = 0 + local mask = 1 + for _ = 1, 32 do + if bitAt(a, mask) == 1 and bitAt(b, mask) == 1 then + result = result + mask + end + mask = mask * 2 + end + return normalizeU32(result) + end + + local function bor32(a, b) + a = normalizeU32(a) + b = normalizeU32(b) + local result = 0 + local mask = 1 + for _ = 1, 32 do + if bitAt(a, mask) == 1 or bitAt(b, mask) == 1 then + result = result + mask + end + mask = mask * 2 + end + return normalizeU32(result) + end + + local function bxor32(a, b) + a = normalizeU32(a) + b = normalizeU32(b) + local result = 0 + local mask = 1 + for _ = 1, 32 do + if bitAt(a, mask) ~= bitAt(b, mask) then + result = result + mask + end + mask = mask * 2 + end + return normalizeU32(result) + end + + local function lshift32(a, disp) + a = normalizeU32(a) + disp = tonumber(disp) or 0 + if disp <= 0 then + return normalizeU32(math.floor(a / (2 ^ (-disp)))) + elseif disp >= 32 then + return 0 + end + return normalizeU32(a * (2 ^ disp)) + end + + local function rshift32(a, disp) + a = normalizeU32(a) + disp = tonumber(disp) or 0 + if disp <= 0 then + return normalizeU32(a * (2 ^ (-disp))) + elseif disp >= 32 then + return 0 + end + return normalizeU32(floor(a / (2 ^ disp))) + end + + local function bnot32(a) + return normalizeU32(U32_MAX - normalizeU32(a)) + end + + bitlib.band = bitlib.band or band32 + bitlib.bor = bitlib.bor or bor32 + bitlib.bxor = bitlib.bxor or bxor32 + bitlib.lshift = bitlib.lshift or lshift32 + bitlib.rshift = bitlib.rshift or rshift32 + bitlib.bnot = bitlib.bnot or bnot32 + bit = bitlib + _G.bit = bitlib +end + -- Shim for Lua 5.0 where string.match is missing. -- Supports up to 5 captures (sufficient for all Questie uses). if not string.match then @@ -57,6 +162,44 @@ if not select then end end +-- Shim for Lua 5.0 where strsplit is missing. +if not strsplit then + strsplit = function(separator, text, max) + if text == nil then + return nil + end + if separator == "" then + return text + end + + local results = {} + local resultCount = 0 + local startPos = 1 + local maxSplits = tonumber(max) + + while true do + if maxSplits and resultCount >= (maxSplits - 1) then + resultCount = resultCount + 1 + results[resultCount] = string.sub(text, startPos) + break + end + + local sepStart, sepEnd = string.find(text, separator, startPos, true) + if not sepStart then + resultCount = resultCount + 1 + results[resultCount] = string.sub(text, startPos) + break + end + + resultCount = resultCount + 1 + results[resultCount] = string.sub(text, startPos, sepStart - 1) + startPos = sepEnd + 1 + end + + return unpack(results, 1, resultCount) + end +end + -- The only public class except for Questie ---@class QuestieLoader QuestieLoader = QuestieLoader or {} diff --git a/Modules/Libs/QuestieSerializer.lua b/Modules/Libs/QuestieSerializer.lua index 46d90b4..654888b 100644 --- a/Modules/Libs/QuestieSerializer.lua +++ b/Modules/Libs/QuestieSerializer.lua @@ -6,6 +6,8 @@ local QuestieSerializer = QuestieLoader:CreateModule("QuestieSerializer"); ---@type QuestieStreamLib local QuestieStreamLib = QuestieLoader:ImportModule("QuestieStreamLib"); +local mod = math.mod + function QuestieSerializer:Hash(value) if not value or type(value) ~= "string" or (string.len(value) <= 0) then @@ -70,14 +72,14 @@ local function floatBitsToInt(n) else expo = expo + 0x7E mant = floor((mant * 2.0 - 1.0) * ldexp(0.5, 24)) - return _pack(sign + floor(expo / 0x2), (expo % 0x2) * 0x80 + floor(mant / 0x10000), floor(mant / 0x100) % 0x100, mant % 0x100) + return _pack(sign + floor(expo / 0x2), mod(expo, 0x2) * 0x80 + floor(mant / 0x10000), mod(floor(mant / 0x100), 0x100), mod(mant, 0x100)) end end local function intBitsToFloat(int) local b1, b2, b3, b4 = _unpack(int) local sign = b1 > 0x7F - local expo = (b1 % 0x80) * 0x2 + floor(b2 / 0x80) - local mant = ((b2 % 0x80) * 0x100 + b3) * 0x100 + b4 + local expo = mod(b1, 0x80) * 0x2 + floor(b2 / 0x80) + local mant = ((mod(b2, 0x80)) * 0x100 + b3) * 0x100 + b4 if sign then sign = -1 else diff --git a/Modules/QuestieStream.lua b/Modules/QuestieStream.lua index 4bdb882..9771fee 100644 --- a/Modules/QuestieStream.lua +++ b/Modules/QuestieStream.lua @@ -10,6 +10,7 @@ local unpack_limit = 4096 -- wow api limits unpack to somewhere between 7000-800 local band = bit.band local lshift = bit.lshift local rshift = bit.rshift +local mod = math.mod local stringchar = string.char local stringbyte = string.byte local stringsub = string.sub @@ -128,7 +129,7 @@ end function QuestieStreamLib:_writeByte(val) local p = self._pointer local chunkId = math.floor((p - 1) / 100000) + 1 - local localIdx = ((p - 1) % 100000) + 1 + local localIdx = mod(p - 1, 100000) + 1 if not self._bin[chunkId] then self._bin[chunkId] = {} @@ -152,7 +153,7 @@ function QuestieStreamLib:_readByte() return stringbyte(self._bin, p) else local chunkId = math.floor((p - 1) / 100000) + 1 - local localIdx = ((p - 1) % 100000) + 1 + local localIdx = mod(p - 1, 100000) + 1 if self._bin[chunkId] then return stringbyte(self._bin[chunkId][localIdx] or "\0") end @@ -184,7 +185,7 @@ function QuestieStreamLib:_WriteByte_b89(e) self._level = level self:_writeByte(QSL_ltab[level]) end - local chr = (e % 86) + 33 + local chr = mod(e, 86) + 33 if QSL_ttab[chr] then self:_writeByte(QSL_ttab[chr]) else @@ -267,7 +268,7 @@ function QuestieStreamLib:_ReadInt12Pair_raw() local p = self._pointer self._pointer = p + 3 local a,b,c = stringbyte(self._bin, p, p+2) - local low4bit = a % 16 + local low4bit = mod(a, 16) return low4bit * 256 + b, (a - low4bit) * 16 + c end @@ -372,8 +373,8 @@ end function QuestieStreamLib:_WriteShort(val) --print("wshort: " .. val); - self:WriteByte(rshift(val, 8) % 256); - self:WriteByte(val % 256); + self:WriteByte(mod(rshift(val, 8), 256)); + self:WriteByte(mod(val, 256)); end function QuestieStreamLib:_WriteShort_assert(val) @@ -384,10 +385,10 @@ function QuestieStreamLib:_WriteShort_assert(val) end function QuestieStreamLib:_WriteInt(val) - self:WriteByte(rshift(val, 24) % 256); - self:WriteByte(rshift(val, 16) % 256); - self:WriteByte(rshift(val, 8) % 256); - self:WriteByte(val % 256); + self:WriteByte(mod(rshift(val, 24), 256)); + self:WriteByte(mod(rshift(val, 16), 256)); + self:WriteByte(mod(rshift(val, 8), 256)); + self:WriteByte(mod(val, 256)); end function QuestieStreamLib:_WriteInt_assert(val) @@ -399,9 +400,9 @@ end function QuestieStreamLib:_WriteInt24(val) --print("wi24: " .. val); - self:WriteByte(rshift(val, 16) % 256); - self:WriteByte(rshift(val, 8) % 256); - self:WriteByte(val % 256); + self:WriteByte(mod(rshift(val, 16), 256)); + self:WriteByte(mod(rshift(val, 8), 256)); + self:WriteByte(mod(val, 256)); end function QuestieStreamLib:_WriteInt24_assert(val) @@ -413,8 +414,8 @@ end function QuestieStreamLib:_WriteInt12Pair(val1, val2) self:WriteByte(band(rshift(val1, 8), 15) + lshift(band(rshift(val2, 8), 15), 4)) - self:WriteByte(val1 % 256) - self:WriteByte(val2 % 256) + self:WriteByte(mod(val1, 256)) + self:WriteByte(mod(val2, 256)) end function QuestieStreamLib:_WriteInt12Pair_assert(val1, val2) @@ -430,14 +431,14 @@ function QuestieStreamLib:_WriteInt12Pair_assert(val1, val2) end function QuestieStreamLib:_WriteLong(val) - self:WriteByte(rshift(val, 56) % 256); - self:WriteByte(rshift(val, 48) % 256); - self:WriteByte(rshift(val, 40) % 256); - self:WriteByte(rshift(val, 32) % 256); - self:WriteByte(rshift(val, 24) % 256); - self:WriteByte(rshift(val, 16) % 256); - self:WriteByte(rshift(val, 8) % 256); - self:WriteByte(val % 256); + self:WriteByte(mod(rshift(val, 56), 256)); + self:WriteByte(mod(rshift(val, 48), 256)); + self:WriteByte(mod(rshift(val, 40), 256)); + self:WriteByte(mod(rshift(val, 32), 256)); + self:WriteByte(mod(rshift(val, 24), 256)); + self:WriteByte(mod(rshift(val, 16), 256)); + self:WriteByte(mod(rshift(val, 8), 256)); + self:WriteByte(mod(val, 256)); end function QuestieStreamLib:_WriteLong_assert(val) @@ -476,7 +477,7 @@ end function QuestieStreamLib:Save() local chunks = {} - for i=1, #self._bin do + for i=1, table.getn(self._bin) do table.insert(chunks, table.concat(self._bin[i])) end return table.concat(chunks) diff --git a/Tests/AuditFindings_spec.lua b/Tests/AuditFindings_spec.lua index 8b6bef1..0838df2 100644 --- a/Tests/AuditFindings_spec.lua +++ b/Tests/AuditFindings_spec.lua @@ -206,7 +206,7 @@ describe("Audit Pass 9 - file-by-file findings (snapshot at HEAD)", function() it("[9.1->11.1] CORRECTED: % modulo IS used (the 'avoided' claim was wrong)", function() -- Pass 8-10 wrongly said % modulo = 0. Real modulo operators exist in -- Turtle-TOC files and are 5.0 parse errors that math.mod cannot rescue. - assert.is_true(has(read("Modules/QuestieStream.lua"), " % 256")) + assert.is_true(has(read("Modules/QuestieStream.lua"), "mod(val, 256)")) assert.is_true(has(read("Modules/QuestiePlayer.lua"), "% playerRaceFlagX2")) end) end) @@ -326,4 +326,25 @@ describe("Audit Pass 10 - additional performance findings (snapshot)", function( assert.is_true(has(advancedOptions, "questieCommsQuestListBlockInterval")) assert.is_true(has(advancedOptions, "Questie.db.profile.questieCommsEnabled == false")) end) + + it("[L50] loader, serializer, and stream are wired for Lua 5.0 compatibility", function() + local loader = read("Modules/Libs/QuestieLoader.lua") + local serializer = read("Modules/Libs/QuestieSerializer.lua") + local stream = read("Modules/QuestieStream.lua") + + assert.is_true(has(loader, "bitlib.band = bitlib.band or band32")) + assert.is_true(has(loader, "strsplit = function(separator, text, max)")) + + assert.is_true(has(serializer, "local mod = math.mod")) + assert.is_true(has(serializer, "mod(expo, 0x2)")) + assert.is_true(has(serializer, "mod(b1, 0x80)")) + assert.is_false(has(serializer, "expo % 0x2")) + + assert.is_true(has(stream, "local mod = math.mod")) + assert.is_true(has(stream, "table.getn(self._bin)")) + assert.is_true(has(stream, "mod(val1, 256)")) + assert.is_true(has(stream, "mod(val2, 256)")) + assert.is_false(has(stream, "val1 % 256")) + assert.is_false(has(stream, "val2 % 256")) + end) end)