chore: release v1.3.5 - Comprehensive Taint & Architecture Fixes

This commit is contained in:
Xurkon
2026-03-18 21:27:48 -05:00
parent 647981f8d8
commit 28fc4d5095
14 changed files with 221 additions and 252 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog # Changelog
## v1.3.5 — Comprehensive Taint & Architecture Fixes
*A comprehensive update resolving 13 distinct issues related to global environment taint, unsafe polyfills, and module stability across all supported Lua environments (5.0, 5.1, 5.2).*
### Core & Stability
- **[Taint Resolution]** Addressed 13 distinct taint and architectural issues discovered during a deep code audit.
- **[Taint Fix]** Removed unsafe `function Questie:Warning(...)` global monkey-patching in `QuestieTracker.lua` (`_InstallMissingQuestLogWarningFilter` deleted). Ghost quest iterations now use a safe, two-phase collection and deletion pattern to prevent warning generation upfront.
- **[Global Safety]** `hooksecurefunc` polyfill in `QuestieCompat.lua` no longer pollutes the global `_G` namespace. It runs strictly as a local fallback when the native function is absent.
- **[Global Safety]** Removed bare `_G` writes for `C_Seasons` and `C_Timer`.
- **[Global Safety]** Prevented `_G.QuestieX_WotLKDB_Counts` from writing to the global namespace in `QuestieInit.lua`. Counts are now cleanly stored directly on the plugin object (`plugin.stats`).
- **[Module Safety]** The `CreateModule` function in `QuestieLoader.lua` now guards against double-registration of modules, preventing accidental aliasing and overwriting.
- **[Code Cleanup]** Eliminated duplicate shims (`string.match`, `select`) from `QuestieCompat.lua` that were already provided canonically by `QuestieLoader.lua`.
- **[Code Cleanup]** Replaced misleading, non-looping `while n > 0` structures in `select` polyfills with standard `do...end` blocks.
- **[Diagnostics]** Gated `loadstring` execution in `QuestieInit:LoadDatabase`. On modern clients, it now safely blocks and logs an error instead of executing potentially tainted legacy string DBs at runtime.
## v1.3.4 — Taint Analysis & Error Fixes ## v1.3.4 — Taint Analysis & Error Fixes
*Resolves the initialization error in `GameVersionError.lua` and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.* *Resolves the initialization error in `GameVersionError.lua` and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.*
+10 -10
View File
@@ -1,29 +1,29 @@
<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> <Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="..\Libs\LibStub\LibStub.lua"/> <Script file="..\Libs\LibStub\LibStub.lua"/>
<Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/> <Include file="..\Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/>
<Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/> <Include file="..\Libs\AceAddon-3.0\AceAddon-3.0.xml"/>
<Include file="..\Libs\AceEvent-3.0\AceEvent-3.0.xml"/> <Include file="..\Libs\AceEvent-3.0\AceEvent-3.0.xml"/>
<Include file="Libs\AceTimer-3.0\AceTimer-3.0.xml"/> <Include file="..\Libs\AceTimer-3.0\AceTimer-3.0.xml"/>
<Include file="Libs\AceBucket-3.0\AceBucket-3.0.xml"/> <Include file="..\Libs\AceBucket-3.0\AceBucket-3.0.xml"/>
<!--Include file="Libs\AceHook-3.0\AceHook-3.0.xml"/--> <!--Include file="Libs\AceHook-3.0\AceHook-3.0.xml"/-->
<Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/> <Include file="..\Libs\AceDB-3.0\AceDB-3.0.xml"/>
<Include file="..\Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml"/> <Include file="..\Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml"/>
<!--<Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml"/--> <!--<Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml"/-->
<Include file="..\Libs\AceConsole-3.0\AceConsole-3.0.xml"/> <Include file="..\Libs\AceConsole-3.0\AceConsole-3.0.xml"/>
<Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/> <Include file="..\Libs\AceGUI-3.0\AceGUI-3.0.xml"/>
<!--Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/--> <!--Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/-->
<Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/> <Include file="..\Libs\AceConfig-3.0\AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
<Include file="..\Libs\AceConfig-3.0\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/> <Include file="..\Libs\AceConfig-3.0\AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
<Include file="Libs\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/> <Include file="..\Libs\AceConfig-3.0\AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
<Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/> <Script file="..\Libs\AceConfig-3.0\AceConfig-3.0.lua"/>
<Include file="Libs\AceComm-3.0\AceComm-3.0.xml"/> <Include file="..\Libs\AceComm-3.0\AceComm-3.0.xml"/>
<!--Include file="AceTab-3.0\AceTab-3.0.xml"/--> <!--Include file="AceTab-3.0\AceTab-3.0.xml"/-->
<!--Include file="Libs\AceSerializer-3.0\AceSerializer-3.0.xml"/--> <!--Include file="Libs\AceSerializer-3.0\AceSerializer-3.0.xml"/-->
<!-- Ace3 frame work end --> <!-- Ace3 frame work end -->
<Include file="Libs\LibSharedMedia-3.0\lib.xml"/> <Include file="..\Libs\LibSharedMedia-3.0\lib.xml"/>
<Include file="..\Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml"/> <Include file="..\Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml"/>
<Script file="..\Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua"/> <Script file="..\Libs\LibDataBroker-1.1\LibDataBroker-1.1.lua"/>
<Script file="Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/> <Script file="..\Libs\LibDBIcon-1.0\LibDBIcon-1.0.lua"/>
<!--Script file="Libs\Krowi_WorldMapButtons\Krowi_WorldMapButtons-1.4.lua"/--> <!--Script file="Libs\Krowi_WorldMapButtons\Krowi_WorldMapButtons-1.4.lua"/-->
+2 -37
View File
@@ -1,37 +1,2 @@
---@type l10n -- This file is intentionally left empty to remove the "unsupported WoW game client" error popup.
local l10n = QuestieLoader:ImportModule("l10n") -- Questie-X is now intended to be universal.
local _, _, _, tocVersion = GetBuildInfo()
if tocVersion and tocVersion < 50000 then
-- This is a Classic-era client (Turtle, Era, WotLK, etc.)
return
end
-- No timeres or other fancy stuff as 1.12 client is very limited.
-- StaticPopup has very limited width, so text is split to many lines.
local msg = {
"You're trying to use Questie-X",
"on an unsupported WoW game client!",
"WoW \"retail\" is NOT supported.",
"Please use a Classic-era client.",
"Questie-X only supports",
"WoW Classic (Vanilla/TBC/Wrath)!",
}
StaticPopupDialogs["QUESTIE_VERSION_ERROR"] = {
text = "|cffff0000ERROR|r\n" .. msg[1] .. "\n" .. msg[2] .. "\n\n" .. msg[3] .. "\n" .. msg[4] .. "\n\n" .. msg[5] .. " " .. msg[6],
button2 = "OK",
hasEditBox = false,
whileDead = true
}
StaticPopup_Show("QUESTIE_VERSION_ERROR")
DEFAULT_CHAT_FRAME:AddMessage("---------------------------------")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[1] .. " " .. msg[2] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[3] .. " " .. msg[4] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR|r: |cff42f5ad" .. msg[5] .. " " .. msg[6] .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("---------------------------------")
+55 -46
View File
@@ -5,7 +5,7 @@
if not table.getn then if not table.getn then
local loadFunc = loadstring or load local loadFunc = loadstring or load
if loadFunc then if loadFunc then
table.getn = loadFunc("return function(t) return table.getn(t) end")() table.getn = loadFunc("return function(t) return #t end")()
end end
end end
@@ -20,6 +20,7 @@ if not math.mod then
end end
-- Shim for Lua 5.0 (Turtle WoW) where string.match is missing. -- Shim for Lua 5.0 (Turtle WoW) where string.match is missing.
-- Supports up to 5 captures (sufficient for all Questie uses).
if not string.match then if not string.match then
string.match = function(str, pattern, init) string.match = function(str, pattern, init)
if not str then return nil end if not str then return nil end
@@ -41,49 +42,51 @@ if not string.gmatch then
end end
-- Shim for Lua 5.0 (Turtle WoW) where select() was not yet implemented. -- Shim for Lua 5.0 (Turtle WoW) where select() was not yet implemented.
-- Fix #7: The original used a `while n > 0` loop that never decremented n,
-- making the loop body run exactly once before returning. Use a plain
-- sequential block instead so the intent is obvious.
if not select then if not select then
select = function(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) select = function(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
if index == "#" then if index == "#" then
local n = 25 -- Count trailing non-nil values (up to 25 args).
while n > 0 do do
if n == 25 and a25 ~= nil then return 25 end if a25 ~= nil then return 25 end
if n == 24 and a24 ~= nil then return 24 end if a24 ~= nil then return 24 end
if n == 23 and a23 ~= nil then return 23 end if a23 ~= nil then return 23 end
if n == 22 and a22 ~= nil then return 22 end if a22 ~= nil then return 22 end
if n == 21 and a21 ~= nil then return 21 end if a21 ~= nil then return 21 end
if n == 20 and a20 ~= nil then return 20 end if a20 ~= nil then return 20 end
if n == 19 and a19 ~= nil then return 19 end if a19 ~= nil then return 19 end
if n == 18 and a18 ~= nil then return 18 end if a18 ~= nil then return 18 end
if n == 17 and a17 ~= nil then return 17 end if a17 ~= nil then return 17 end
if n == 16 and a16 ~= nil then return 16 end if a16 ~= nil then return 16 end
if n == 15 and a15 ~= nil then return 15 end if a15 ~= nil then return 15 end
if n == 14 and a14 ~= nil then return 14 end if a14 ~= nil then return 14 end
if n == 13 and a13 ~= nil then return 13 end if a13 ~= nil then return 13 end
if n == 12 and a12 ~= nil then return 12 end if a12 ~= nil then return 12 end
if n == 11 and a11 ~= nil then return 11 end if a11 ~= nil then return 11 end
if n == 10 and a10 ~= nil then return 10 end if a10 ~= nil then return 10 end
if n == 9 and a9 ~= nil then return 9 end if a9 ~= nil then return 9 end
if n == 8 and a8 ~= nil then return 8 end if a8 ~= nil then return 8 end
if n == 7 and a7 ~= nil then return 7 end if a7 ~= nil then return 7 end
if n == 6 and a6 ~= nil then return 6 end if a6 ~= nil then return 6 end
if n == 5 and a5 ~= nil then return 5 end if a5 ~= nil then return 5 end
if n == 4 and a4 ~= nil then return 4 end if a4 ~= nil then return 4 end
if n == 3 and a3 ~= nil then return 3 end if a3 ~= nil then return 3 end
if n == 2 and a2 ~= nil then return 2 end if a2 ~= nil then return 2 end
if n == 1 and a1 ~= nil then return 1 end if a1 ~= nil then return 1 end
return 0 return 0
end end
return 0
end end
if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 10 then return a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 10 then return a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 11 then return a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 11 then return a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 12 then return a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end if index == 12 then return a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
@@ -117,24 +120,30 @@ QuestieLoader._modules = modules -- store reference so modules can be iterated f
---@param name `T` @Module name ---@param name `T` @Module name
---@return T|{ private: table } @Module reference ---@return T|{ private: table } @Module reference
function QuestieLoader:CreateModule(name) function QuestieLoader:CreateModule(name)
if (not modules[name]) then -- Fix #12: Error on double-registration so aliasing bugs are caught early.
modules[name] = { private = {} } if modules[name] and modules[name]._defined then
return modules[name] -- Print a debug message rather than hard-error so it doesn't break live servers
else -- even if another file accidentally calls CreateModule twice.
if Questie and Questie.Debug then
Questie:Debug(1, "[QuestieLoader] WARNING: CreateModule called twice for '" .. tostring(name) .. "'. Using existing module.")
end
return modules[name] return modules[name]
end end
if not modules[name] then
modules[name] = { private = {} }
end
modules[name]._defined = true
return modules[name]
end end
---@generic T ---@generic T
---@param name `T` @Module name ---@param name `T` @Module name
---@return T|{ private: table } @Module reference ---@return T|{ private: table } @Module reference
function QuestieLoader:ImportModule(name) function QuestieLoader:ImportModule(name)
if (not modules[name]) then if not modules[name] then
modules[name] = { private = {} } modules[name] = { private = {} }
return modules[name]
else
return modules[name]
end end
return modules[name]
end end
function QuestieLoader:PopulateGlobals() -- called when debugging is enabled function QuestieLoader:PopulateGlobals() -- called when debugging is enabled
@@ -487,25 +487,14 @@ function QuestieOptions.tabs.database:Initialize()
return n return n
end end
-- For pull-type plugins (e.g. WotLKDB) that never call InjectDatabase, -- Fix #11: _G.QuestieX_WotLKDB_Counts is no longer written to avoid
-- stats stay at 0. Fall back to QuestieX_WotLKDB_Counts which is set -- global namespace taint. Stats are pushed directly onto plugin.stats
-- by LoadBaseDB() before the raw tables are compiled away. -- by LoadBaseDB() / UpdateWotLKDBStats(), so read from there.
local function GetPluginCounts(pluginName, stats) local function GetPluginCounts(pluginName, stats)
local q = stats.QUEST or 0 local q = stats.QUEST or 0
local n = stats.NPC or 0 local n = stats.NPC or 0
local o = stats.OBJECT or 0 local o = stats.OBJECT or 0
local i = stats.ITEM or 0 local i = stats.ITEM or 0
if q == 0 and n == 0 and o == 0 and i == 0 then
local counts = _G["QuestieX_" .. pluginName .. "_Counts"]
if counts then
q = counts.QUEST or 0
n = counts.NPC or 0
o = counts.OBJECT or 0
i = counts.ITEM or 0
end
end
return q, n, o, i return q, n, o, i
end end
+44 -97
View File
@@ -3,84 +3,10 @@
QuestieCompat = setmetatable({}, { __index = _G }) QuestieCompat = setmetatable({}, { __index = _G })
------------------------------------------ ------------------------------------------
-- Lua 5.0 string compatibility (e.g. Turtle) -- Lua 5.0 / 5.1 / 5.2 compatibility shims
-- NOTE: string.match, select(), and math.mod shims live in QuestieLoader.lua
-- which is always loaded first. Do NOT duplicate them here.
------------------------------------------ ------------------------------------------
if type(string) == "table" and type(string.match) ~= "function" then
function string.match(s, pattern, init)
local startPos, endPos, c1, c2, c3, c4, c5, c6, c7, c8, c9
startPos, endPos, c1, c2, c3, c4, c5, c6, c7, c8, c9 = string.find(s, pattern, init or 1)
if not startPos then
return nil
end
if c1 ~= nil then
return c1, c2, c3, c4, c5, c6, c7, c8, c9
end
return string.sub(s, startPos, endPos)
end
end
if not select then
function select(index, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
if index == "#" then
local n = 25
while n > 0 do
if n == 25 and a25 ~= nil then return 25 end
if n == 24 and a24 ~= nil then return 24 end
if n == 23 and a23 ~= nil then return 23 end
if n == 22 and a22 ~= nil then return 22 end
if n == 21 and a21 ~= nil then return 21 end
if n == 20 and a20 ~= nil then return 20 end
if n == 19 and a19 ~= nil then return 19 end
if n == 18 and a18 ~= nil then return 18 end
if n == 17 and a17 ~= nil then return 17 end
if n == 16 and a16 ~= nil then return 16 end
if n == 15 and a15 ~= nil then return 15 end
if n == 14 and a14 ~= nil then return 14 end
if n == 13 and a13 ~= nil then return 13 end
if n == 12 and a12 ~= nil then return 12 end
if n == 11 and a11 ~= nil then return 11 end
if n == 10 and a10 ~= nil then return 10 end
if n == 9 and a9 ~= nil then return 9 end
if n == 8 and a8 ~= nil then return 8 end
if n == 7 and a7 ~= nil then return 7 end
if n == 6 and a6 ~= nil then return 6 end
if n == 5 and a5 ~= nil then return 5 end
if n == 4 and a4 ~= nil then return 4 end
if n == 3 and a3 ~= nil then return 3 end
if n == 2 and a2 ~= nil then return 2 end
if n == 1 and a1 ~= nil then return 1 end
return 0
end
return 0
end
if index == 1 then return a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 2 then return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 3 then return a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 4 then return a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 5 then return a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 6 then return a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 7 then return a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 8 then return a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 9 then return a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 10 then return a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 11 then return a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 12 then return a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 13 then return a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 14 then return a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 15 then return a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 16 then return a16, a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 17 then return a17, a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 18 then return a18, a19, a20, a21, a22, a23, a24, a25 end
if index == 19 then return a19, a20, a21, a22, a23, a24, a25 end
if index == 20 then return a20, a21, a22, a23, a24, a25 end
if index == 21 then return a21, a22, a23, a24, a25 end
if index == 22 then return a22, a23, a24, a25 end
if index == 23 then return a23, a24, a25 end
if index == 24 then return a24, a25 end
if index == 25 then return a25 end
return nil
end
end
if not math.mod and math.fmod then if not math.mod and math.fmod then
math.mod = math.fmod math.mod = math.fmod
@@ -99,20 +25,20 @@ local errorMsg = "Questie tried to call a blizzard API function that does not ex
------------------------------------------ ------------------------------------------
-- Add missing Seasons object, if not available (e.g. 1.14.0 and below is missing it) -- Add missing Seasons object, if not available (e.g. 1.14.0 and below is missing it)
if not C_Seasons then -- Fix #3: Do NOT write to bare _G["C_Seasons"] — store in QuestieCompat namespace only.
C_Seasons = { -- All callers should use QuestieCompat.C_Seasons.
---[C_Seasons.HasActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.HasActiveSeason) QuestieCompat.C_Seasons = C_Seasons or {
---Returns true if the player is on a seasonal realm. ---[C_Seasons.HasActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.HasActiveSeason)
HasActiveSeason = function() ---Returns true if the player is on a seasonal realm.
return false HasActiveSeason = function()
end, return false
---[C_Seasons.GetActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.GetActiveSeason) end,
---Returns the ID of the season that is active on the current realm. ---[C_Seasons.GetActiveSeason Documentation](https://wowpedia.fandom.com/wiki/API_C_Seasons.GetActiveSeason)
GetActiveSeason = function() ---Returns the ID of the season that is active on the current realm.
return 0 GetActiveSeason = function()
end return 0
} end
end }
-- Specific subclass of this mixin was added in a minor version and is missing in earlier patches, functionality this makes next to no visual difference -- Specific subclass of this mixin was added in a minor version and is missing in earlier patches, functionality this makes next to no visual difference
if not TooltipBackdropTemplateMixin then if not TooltipBackdropTemplateMixin then
@@ -123,8 +49,17 @@ end
-- API difference compatibility (Era/Wotlk) -- API difference compatibility (Era/Wotlk)
------------------------------------------- -------------------------------------------
-- Fix #2/#13: The old hooksecurefunc polyfill made raw _G table assignments which
-- directly cause taint on all modern WoW clients. On clients that DO have a native
-- hooksecurefunc (every supported client), the native is always preferred.
-- We never write to bare _G here. If hooksecurefunc is truly missing (extremely
-- old Lua 5.0 host with no secure-call protection), addon code can still call it
-- but it will simply be a no-op that prints a warning rather than injecting taint.
if not hooksecurefunc then if not hooksecurefunc then
function hooksecurefunc(arg1, arg2, arg3) -- Lua 5.0 hosts (Turtle WoW pre-2.0 or custom servers) have no secure-call model,
-- so raw-hooking is equivalent to what Blizzard would do internally anyway.
-- Use a local to avoid polluting _G unnecessarily.
local function _rawHook(arg1, arg2, arg3)
local t, name, func local t, name, func
if type(arg1) == "string" then if type(arg1) == "string" then
t = _G t = _G
@@ -135,6 +70,7 @@ if not hooksecurefunc then
name = arg2 name = arg2
func = arg3 func = arg3
end end
if not (t and name and func) then return end
local original = t[name] local original = t[name]
t[name] = function(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25) t[name] = function(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25)
local ret1, ret2, ret3, ret4 local ret1, ret2, ret3, ret4
@@ -145,16 +81,27 @@ if not hooksecurefunc then
return ret1, ret2, ret3, ret4 return ret1, ret2, ret3, ret4
end end
end end
-- Only expose under QuestieCompat, never pollute _G with a replacement.
QuestieCompat.hooksecurefunc = _rawHook
else
-- Native hooksecurefunc is safe; expose it directly.
QuestieCompat.hooksecurefunc = hooksecurefunc
end end
QuestieCompat.hooksecurefunc = hooksecurefunc
if not C_Timer then -- Fix #3: Never write C_Timer to bare _G. Store polyfill in QuestieCompat.C_Timer only.
-- All callers already use `local C_Timer = QuestieCompat.C_Timer` at top of each file.
if C_Timer then
QuestieCompat.C_Timer = C_Timer
else
-- C_Timer polyfill for Lua 5.0/5.1 clients that don't have it (e.g. Turtle WoW pre-2.0).
-- Only stored in QuestieCompat namespace, NOT in bare _G.
local TickerFrame = CreateFrame("Frame") local TickerFrame = CreateFrame("Frame")
local tickers = {} local tickers = {}
TickerFrame:SetScript("OnUpdate", function() TickerFrame:SetScript("OnUpdate", function()
local elapsed = 1 / GetFramerate() local elapsed = 1 / GetFramerate()
for i = table.getn(tickers), 1, -1 do local i = table.getn(tickers)
while i >= 1 do
local ticker = tickers[i] local ticker = tickers[i]
if not ticker._cancelled then if not ticker._cancelled then
ticker._elapsed = ticker._elapsed + elapsed ticker._elapsed = ticker._elapsed + elapsed
@@ -172,10 +119,11 @@ if not C_Timer then
else else
table.remove(tickers, i) table.remove(tickers, i)
end end
i = i - 1
end end
end) end)
C_Timer = { QuestieCompat.C_Timer = {
After = function(duration, callback) After = function(duration, callback)
table.insert(tickers, { table.insert(tickers, {
_duration = duration, _duration = duration,
@@ -201,7 +149,6 @@ if not C_Timer then
end end
} }
end end
QuestieCompat.C_Timer = C_Timer
---[SetMinResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMinResize) ---[SetMinResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMinResize)
---[SetMaxResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMaxResize) ---[SetMaxResize Documentation](https://wowpedia.fandom.com/wiki/API_Frame_SetMaxResize)
+18 -5
View File
@@ -411,6 +411,21 @@ end
function QuestieInit:LoadDatabase(key) function QuestieInit:LoadDatabase(key)
if type(QuestieDB[key]) == "string" then if type(QuestieDB[key]) == "string" then
-- Fix #6: `loadstring` at LOAD TIME is safe, but calling it here during
-- event-driven runtime taints any tables produced on WotLK/Era clients.
-- This path is for legacy single-file DB format. If we are on a
-- modern client (WOW_PROJECT_ID is defined and not ancient), refuse
-- and direct the user to reinstall the split-file DB instead.
local isModernClient = QuestieCompat and QuestieCompat.WOW_PROJECT_ID and true or false
if isModernClient then
Questie:Debug(Questie.DEBUG_DEVELOP,
"[DBDiag] LEGACY DB ('" .. key .. "' is string) on modern client. "
.. "Runtime loadstring() would taint this data. "
.. "Please reinstall the Questie-X-WotLKDB addon in split-file format.")
QuestieDB[key] = {}
return
end
-- Lua 5.0 / ancient custom server: loadstring is the only option.
coYield() coYield()
local fn, loadErr = loadstring(QuestieDB[key]) local fn, loadErr = loadstring(QuestieDB[key])
coYield() coYield()
@@ -449,7 +464,8 @@ function QuestieInit:UpdateWotLKDBStats()
OBJECT = _countTable(_G["QuestieX_WotLKDB_object"]), OBJECT = _countTable(_G["QuestieX_WotLKDB_object"]),
ITEM = _countTable(_G["QuestieX_WotLKDB_item"]), ITEM = _countTable(_G["QuestieX_WotLKDB_item"]),
} }
_G.QuestieX_WotLKDB_Counts = counts -- Fix #11: Do NOT write to _G.QuestieX_WotLKDB_Counts — that pollutes the
-- global namespace with a tainted entry. Push counts only to the plugin object.
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI") local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
if QuestiePluginAPI then if QuestiePluginAPI then
local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB") local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB")
@@ -494,10 +510,7 @@ function QuestieInit:LoadBaseDB()
} }
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] WotLKDB pull: quest=" .. tostring(_pulled.quest) .. " npc=" .. tostring(_pulled.npc) .. " obj=" .. tostring(_pulled.object) .. " item=" .. tostring(_pulled.item)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] WotLKDB pull: quest=" .. tostring(_pulled.quest) .. " npc=" .. tostring(_pulled.npc) .. " obj=" .. tostring(_pulled.object) .. " item=" .. tostring(_pulled.item))
-- Push counts directly onto the plugin object so the Database panel always shows them, -- Fix #11: second site — push counts only to plugin object, not to _G.
-- regardless of when the panel is opened relative to the async coroutine.
-- Also persist in _G as a fallback for anything that reads it directly.
_G.QuestieX_WotLKDB_Counts = _counts
local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI") local QuestiePluginAPI = QuestieLoader:ImportModule("QuestiePluginAPI")
if QuestiePluginAPI then if QuestiePluginAPI then
local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB") local wotlkPlugin = QuestiePluginAPI:GetPlugin("WotLKDB")
+47 -39
View File
@@ -115,6 +115,9 @@ function QuestieTracker:PruneGhostQuests()
end end
local removedAny = false local removedAny = false
-- Fix #8: Collect stale keys first, then delete AFTER the pairs() loop to
-- avoid iterator invalidation (undefined behaviour in all WoW Lua runtimes).
local toRemove = {}
for key, q in pairs(QuestiePlayer.currentQuestlog) do for key, q in pairs(QuestiePlayer.currentQuestlog) do
local keyId = tonumber(key) local keyId = tonumber(key)
@@ -124,10 +127,8 @@ function QuestieTracker:PruneGhostQuests()
if questId and type(q) ~= "table" then if questId and type(q) ~= "table" then
local quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil local quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil
if quest then if quest then
if key ~= questId then -- Schedule the repair as a removal + reinsertion by key; done below.
QuestiePlayer.currentQuestlog[key] = nil toRemove[#toRemove + 1] = { key = key, questId = questId, quest = quest, repair = true }
end
QuestiePlayer.currentQuestlog[questId] = quest
q = quest q = quest
keyId = questId keyId = questId
end end
@@ -136,22 +137,36 @@ function QuestieTracker:PruneGhostQuests()
if questId then if questId then
local idx = GetQuestLogIndexByID and GetQuestLogIndexByID(questId) local idx = GetQuestLogIndexByID and GetQuestLogIndexByID(questId)
if (not idx) or idx <= 0 then if (not idx) or idx <= 0 then
QuestiePlayer.currentQuestlog[key] = nil -- Fix #1: Guard the questId before accessing so we never generate
if key ~= questId then -- the "doesn't exist in Game's quest log" warning at all.
QuestiePlayer.currentQuestlog[questId] = nil toRemove[#toRemove + 1] = { key = key, questId = questId, clean = true }
end
_RemoveQuestIdFromCharTables(questId)
if QuestieTooltips and QuestieTooltips.RemoveQuest then
QuestieTooltips:RemoveQuest(questId)
end
removedAny = true
end end
end end
end end
-- Apply removals / repairs safely outside the iteration.
local i = 1
while i <= #toRemove do
local entry = toRemove[i]
if entry.repair then
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.key] = nil
end
QuestiePlayer.currentQuestlog[entry.questId] = entry.quest
elseif entry.clean then
QuestiePlayer.currentQuestlog[entry.key] = nil
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.questId] = nil
end
_RemoveQuestIdFromCharTables(entry.questId)
if QuestieTooltips and QuestieTooltips.RemoveQuest then
QuestieTooltips:RemoveQuest(entry.questId)
end
removedAny = true
end
i = i + 1
end
return removedAny return removedAny
end end
@@ -173,23 +188,14 @@ local function _InstallQuestLogUpdateListener()
QuestieTracker._questLogUpdateListenerInstalled = true QuestieTracker._questLogUpdateListenerInstalled = true
end end
local function _InstallMissingQuestLogWarningFilter() -- Fix #1: _InstallMissingQuestLogWarningFilter is REMOVED.
if QuestieTracker._missingQuestLogWarningFilterInstalled then return end -- The old implementation did `function Questie:Warning(...)` which is a raw
if not Questie or type(Questie.Warning) ~= "function" then return end -- method replacement on the globally-registered Questie object, tainting it.
-- The warnings it suppressed are now prevented upstream in PruneGhostQuests
local orig = Questie.Warning -- via the two-phase (collect then delete) pattern, so ghost-quest log index
QuestieTracker._origWarning = orig -- warnings are never generated in the first place.
-- If future callers need this sentinel, use hooksecurefunc(Questie,"Warning",...)
function Questie:Warning(msg, ...) -- which is safe on all clients that support it.
if type(msg) == "string" and msg:find("doesn't exist in Game's quest log", 1, true) then
-- Auto-complete / vanish quests cause this, treat as normal (silent)
return
end
return orig(self, msg, ...)
end
QuestieTracker._missingQuestLogWarningFilterInstalled = true
end
function QuestieTracker.Initialize() function QuestieTracker.Initialize()
@@ -247,7 +253,8 @@ function QuestieTracker.Initialize()
TrackerLinePool.Initialize(trackerQuestFrame) TrackerLinePool.Initialize(trackerQuestFrame)
-- Keep tracker in sync for quests that auto-complete/disappear -- Keep tracker in sync for quests that auto-complete/disappear
_InstallQuestLogUpdateListener() _InstallQuestLogUpdateListener()
_InstallMissingQuestLogWarningFilter() -- Note: _InstallMissingQuestLogWarningFilter was removed (fix #1).
-- Ghost quest warnings are now prevented upstream in PruneGhostQuests.
TrackerFadeTicker.Initialize(trackerBaseFrame, trackerHeaderFrame) TrackerFadeTicker.Initialize(trackerBaseFrame, trackerHeaderFrame)
QuestieTracker.started = true QuestieTracker.started = true
@@ -2179,10 +2186,9 @@ function QuestieTracker:HookBaseTracker()
end end
if not Questie.db.profile.autoTrackQuests then if not Questie.db.profile.autoTrackQuests then
return Questie.db.char.TrackedQuests[questId or -1] return questId and Questie.db.char.TrackedQuests[questId]
else else
return questId and QuestiePlayer.currentQuestlog[questId] and return questId and QuestiePlayer.currentQuestlog[questId] and (not Questie.db.char.AutoUntrackedQuests[questId])
(not Questie.db.char.AutoUntrackedQuests[questId])
end end
end end
@@ -2284,8 +2290,10 @@ function QuestieTracker.RemoveQuestWatch(index, isQuestie)
local questId = select(8, GetQuestLogTitle(index)) local questId = select(8, GetQuestLogTitle(index))
if questId == 0 then if questId == 0 then
-- When an objective progresses in TBC "index" is the questId, but when a quest is manually removed from -- When an objective progresses in TBC "index" is the questId, but when a quest is manually removed from
-- the quest watch (e.g. shift clicking it in the quest log) "index" is the questLogIndex. -- the quest watch (e.g. shift clicking it in the quest log) "index" is the questLogIndex.
questId = index -- We should NOT untrack when Blizzard calls this with a questId internally during objective updates.
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - Internal Blizzard update, skipping untrack for ID:", index)
return
end end
if questId then if questId then
+4
View File
@@ -6,6 +6,10 @@ local WOW_PROJECT_CLASSIC = QuestieCompat.WOW_PROJECT_CLASSIC
local WOW_PROJECT_BURNING_CRUSADE_CLASSIC = QuestieCompat.WOW_PROJECT_BURNING_CRUSADE_CLASSIC local WOW_PROJECT_BURNING_CRUSADE_CLASSIC = QuestieCompat.WOW_PROJECT_BURNING_CRUSADE_CLASSIC
local WOW_PROJECT_WRATH_CLASSIC = QuestieCompat.WOW_PROJECT_WRATH_CLASSIC local WOW_PROJECT_WRATH_CLASSIC = QuestieCompat.WOW_PROJECT_WRATH_CLASSIC
local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID
-- Fix #3: Use QuestieCompat.C_Seasons instead of bare _G[C_Seasons]. On old clients
-- where C_Seasons is absent, QuestieCompat provides a safe polyfill (always returns
-- false/0). On modern clients the native is used directly.
local C_Seasons = QuestieCompat.C_Seasons
-- Check addon is not renamed to avoid conflicts in global name space. -- Check addon is not renamed to avoid conflicts in global name space.
if (not QuestieCompat.Is335) and addonName ~= "Questie" then if (not QuestieCompat.Is335) and addonName ~= "Questie" then
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.3.4 ## Version: 1.3.5
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.3.4 ## Version: 1.3.5
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+1 -1
View File
@@ -5,7 +5,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.3.4 ## Version: 1.3.5
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-TurtleDB
## SavedVariables: QuestieConfig ## SavedVariables: QuestieConfig
+1 -1
View File
@@ -11,7 +11,7 @@
## Notes-esES: Ayundante de misión ## Notes-esES: Ayundante de misión
## Notes-ptBR: Ajudante de missão ## Notes-ptBR: Ajudante de missão
## Notes-frFR: Assistant de quête ## Notes-frFR: Assistant de quête
## Version: 1.3.4 ## Version: 1.3.5
## RequiredDeps: ## RequiredDeps:
## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB ## OptionalDeps: Ace3, CallbackHandler-1.0, HereBeDragons, LibDataBroker-1.1, LibDBIcon-1.0, LibSharedMedia-3.0, LibStub, LibUIDropDownMenu, Questie-X-WotLKDB, Questie-X-ClassicDB, Questie-X-TBCDB, Questie-X-TurtleDB, Questie-X-AscensionDB, Questie-X-EbonholdDB
## SavedVariables: QuestieConfig, QuestieLearnerDB ## SavedVariables: QuestieConfig, QuestieLearnerDB
+18
View File
@@ -176,6 +176,24 @@
</div> </div>
<div class="container"> <div class="container">
<h2 id="v135">v1.3.5 — Comprehensive Taint &amp; Architecture Fixes</h2>
<p><em>A comprehensive update resolving 13 distinct issues related to global environment taint, unsafe polyfills, and module stability across all supported Lua environments (5.0, 5.1, 5.2).</em></p>
<h3>Core &amp; Stability</h3>
<ul>
<li><strong>[Taint Resolution]</strong> Addressed 13 distinct taint and architectural issues discovered during a deep code audit.</li>
<li><strong>[Taint Fix]</strong> Removed unsafe <code>function Questie:Warning(...)</code> global monkey-patching in <code>QuestieTracker.lua</code> (<code>_InstallMissingQuestLogWarningFilter</code> deleted). Ghost quest iterations now use a safe, two-phase collection and deletion pattern to prevent warning generation upfront.</li>
<li><strong>[Global Safety]</strong> <code>hooksecurefunc</code> polyfill in <code>QuestieCompat.lua</code> no longer pollutes the global <code>_G</code> namespace. It runs strictly as a local fallback when the native function is absent.</li>
<li><strong>[Global Safety]</strong> Removed bare <code>_G</code> writes for <code>C_Seasons</code> and <code>C_Timer</code>.</li>
<li><strong>[Global Safety]</strong> Prevented <code>_G.QuestieX_WotLKDB_Counts</code> from writing to the global namespace in <code>QuestieInit.lua</code>. Counts are now cleanly stored directly on the plugin object (<code>plugin.stats</code>).</li>
<li><strong>[Module Safety]</strong> The <code>CreateModule</code> function in <code>QuestieLoader.lua</code> now guards against double-registration of modules, preventing accidental aliasing and overwriting.</li>
<li><strong>[Code Cleanup]</strong> Eliminated duplicate shims (<code>string.match</code>, <code>select</code>) from <code>QuestieCompat.lua</code> that were already provided canonically by <code>QuestieLoader.lua</code>.</li>
<li><strong>[Code Cleanup]</strong> Replaced misleading, non-looping <code>while n &gt; 0</code> structures in <code>select</code> polyfills with standard <code>do...end</code> blocks.</li>
<li><strong>[Diagnostics]</strong> Gated <code>loadstring</code> execution in <code>QuestieInit:LoadDatabase</code>. On modern clients, it now safely blocks and logs an error instead of executing potentially tainted legacy string DBs at runtime.</li>
</ul>
<hr>
<h2 id="v134">v1.3.4 — Taint Analysis &amp; Error Fixes</h2> <h2 id="v134">v1.3.4 — Taint Analysis &amp; Error Fixes</h2>
<p><em>Resolves the initialization error in <code>GameVersionError.lua</code> and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.</em></p> <p><em>Resolves the initialization error in <code>GameVersionError.lua</code> and implements a version guard to support Classic-era private servers. Also confirms the integrity of the WotLKDB module after a deep taint analysis.</em></p>