From 246407183eb1ca61648799a2b43cc0de028a8eb8 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 11 Nov 2025 10:29:35 -0500 Subject: [PATCH] intercept addon errors from string parsing malformed macros --- MacroErrorChecker.lua | 521 ++++++++++++++++++++++++++---------------- 1 file changed, 326 insertions(+), 195 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index 88b28ba..eb6ca93 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -11,41 +11,31 @@ local CleveRoids = _G.CleveRoids or {} -- Known valid conditionals local VALID_CONDITIONALS = { -- General - actionbar = true, noactionbar = true, - button = true, channeling = true, nochanneling = true, class = true, noclass = true, cdgcd = true, nocdgcd = true, combo = true, nocombo = true, cooldown = true, nocooldown = true, equipped = true, noequipped = true, - flyable = true, noflyable = true, - flying = true, noflying = true, form = true, noform = true, - group = true, nogroup = true, - indoors = true, noindoors = true, + group = true, known = true, noknown = true, mod = true, nomod = true, - mounted = true, nomounted = true, mybuff = true, nomybuff = true, mydebuff = true, nomydebuff = true, - myhp = true, nomyhp = true, - myhplost = true, nomyhplost = true, - mypower = true, nomypower = true, - mypowerlost = true, nomypowerlost = true, - myrawhp = true, nomyrawhp = true, - myrawpower = true, nomyrawpower = true, - outdoors = true, nooutdoors = true, + myhp = true, + myhplost = true, + mypower = true, + mypowerlost = true, + myrawhp = true, + myrawpower = true, pet = true, nopet = true, - petbuff = true, nopetbuff = true, - petdebuff = true, nopetdebuff = true, reactive = true, noreactive = true, resting = true, noresting = true, stance = true, nostance = true, - stat = true, nostat = true, + stat = true, stealth = true, nostealth = true, swimming = true, noswimming = true, - talent = true, notalent = true, zone = true, nozone = true, -- Unit based @@ -57,18 +47,18 @@ local VALID_CONDITIONALS = { debuff = true, nodebuff = true, harm = true, noharm = true, help = true, nohelp = true, - hp = true, nohp = true, - hplost = true, nohplost = true, + hp = true, + hplost = true, inrange = true, noinrange = true, - isnpc = true, noisnpc = true, - isplayer = true, noisplayer = true, - member = true, nomember = true, + isnpc = true, + isplayer = true, + member = true, party = true, noparty = true, - power = true, nopower = true, - powerlost = true, nopowerlost = true, + power = true, + powerlost = true, raid = true, noraid = true, - rawhp = true, norawhp = true, - rawpower = true, norawpower = true, + rawhp = true, + rawpower = true, type = true, notype = true, targeting = true, notargeting = true, exists = true, noexists = true, @@ -106,6 +96,36 @@ local VALID_COMMANDS = { ["/retarget"] = true, } +-- Safe string operations to prevent addon errors from malformed macros +local function safeStringSub(str, startPos, endPos) + if not str or type(str) ~= "string" then return "" end + local len = string.len(str) + if startPos < 1 then startPos = 1 end + if endPos and endPos > len then endPos = len end + return string.sub(str, startPos, endPos) +end + +local function safeStringFind(str, pattern, init) + if not str or type(str) ~= "string" then return nil end + local success, result1, result2, result3 = pcall(string.find, str, pattern, init) + if success then + return result1, result2, result3 + end + return nil +end + +local function safeStringLen(str) + if not str or type(str) ~= "string" then return 0 end + return string.len(str) +end + +local function safeTrim(str) + if not str or type(str) ~= "string" then return "" end + local success, result = pcall(CleveRoids.Trim, str) + if success then return result end + return str +end + -- Error types local ERROR_TYPES = { INVALID_CONDITIONAL = "Invalid conditional", @@ -124,11 +144,15 @@ CleveRoids.MacroErrors = {} -- Check if a string has balanced brackets local function checkBrackets(text) + if not text or type(text) ~= "string" then return true end + local openCount = 0 local inQuotes = false + local len = safeStringLen(text) - for i = 1, string.len(text) do - local char = string.sub(text, i, i) + for i = 1, len do + local char = safeStringSub(text, i, i) + if not char or char == "" then break end if char == '"' then inQuotes = not inQuotes @@ -155,11 +179,15 @@ end -- Check if quotes are balanced local function checkQuotes(text) + if not text or type(text) ~= "string" then return true end + local quoteCount = 0 local escaped = false + local len = safeStringLen(text) - for i = 1, string.len(text) do - local char = string.sub(text, i, i) + for i = 1, len do + local char = safeStringSub(text, i, i) + if not char or char == "" then break end if escaped then escaped = false @@ -181,9 +209,13 @@ end -- Validate conditional syntax local function validateConditional(conditional, args, action) local errors = {} + + if not conditional or conditional == "" then + return errors + end -- Check if conditional is valid - local baseCond = string.lower(conditional) + local baseCond = string.lower(safeTrim(conditional)) if not VALID_CONDITIONALS[baseCond] then table.insert(errors, { type = ERROR_TYPES.INVALID_CONDITIONAL, @@ -215,17 +247,20 @@ local function validateConditional(conditional, args, action) end -- Check operator syntax for numeric comparisons - if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or - string.find(baseCond, "combo") or baseCond == "stat" then - local hasOperator = string.find(args, "[<>=~]+") - if args ~= "" and not hasOperator and not string.find(args, "^%d+$") then - -- Might be missing operator - if not string.find(args, "[a-zA-Z]") then - table.insert(errors, { - type = ERROR_TYPES.INVALID_OPERATOR, - conditional = conditional, - message = conditional .. " may need an operator (>, <, =, >=, <=)" - }) + if args and type(args) == "string" then + local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or + safeStringFind(baseCond, "combo") or baseCond == "stat" + if hasHpOrPower then + local hasOperator = safeStringFind(args, "[<>=~]+") + if args ~= "" and not hasOperator and not safeStringFind(args, "^%d+$") then + -- Might be missing operator + if not safeStringFind(args, "[a-zA-Z]") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_OPERATOR, + conditional = conditional, + message = conditional .. " may need an operator (>, <, =, >=, <=)" + }) + end end end end @@ -238,130 +273,164 @@ local function validateLine(line, lineNum) local errors = {} -- Skip comments and empty lines - line = CleveRoids.Trim(line) - if line == "" or string.sub(line, 1, 2) == "--" or string.sub(line, 1, 1) == "#" then + if not line or type(line) ~= "string" then return errors end - -- Check for valid command - local _, _, cmd = string.find(line, "^(/[a-z]+)") - if cmd then - local lowerCmd = string.lower(cmd) - if not VALID_COMMANDS[lowerCmd] then - table.insert(errors, { - type = ERROR_TYPES.INVALID_COMMAND, - line = lineNum, - command = cmd, - message = "Unknown command: " .. cmd - }) - end + line = safeTrim(line) + if line == "" or safeStringSub(line, 1, 2) == "--" or safeStringSub(line, 1, 1) == "#" then + return errors end - -- Check brackets - local bracketsOk, bracketError = checkBrackets(line) - if not bracketsOk then - table.insert(errors, { - type = ERROR_TYPES.MISMATCHED_BRACKETS, - line = lineNum, - message = bracketError - }) - end + -- Wrap the entire validation in pcall to catch any unexpected errors + local success, result = pcall(function() + local localErrors = {} - -- Check quotes - local quotesOk, quoteError = checkQuotes(line) - if not quotesOk then - table.insert(errors, { - type = ERROR_TYPES.MALFORMED_QUOTES, - line = lineNum, - message = quoteError - }) - end - - -- Split by semicolons to handle multiple actions per line - local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") - - for _, actionPart in ipairs(actions) do - actionPart = CleveRoids.Trim(actionPart) - if actionPart ~= "" and string.sub(actionPart, 1, 1) ~= "/" then - actionPart = "/" .. actionPart -- Add leading slash if missing after split - end - - -- Parse conditionals if present - use non-greedy match - local condStart = string.find(actionPart, "%[") - local condEnd = nil - local conditionBlock = nil - - if condStart then - -- Find matching closing bracket - local depth = 0 - local inQuotes = false - for i = condStart, string.len(actionPart) do - local char = string.sub(actionPart, i, i) - if char == '"' then - inQuotes = not inQuotes - elseif not inQuotes then - if char == "[" then - depth = depth + 1 - elseif char == "]" then - depth = depth - 1 - if depth == 0 then - condEnd = i - conditionBlock = string.sub(actionPart, condStart + 1, i - 1) - break - end - end - end + -- Check for valid command + local _, _, cmd = safeStringFind(line, "^(/[a-z]+)") + if cmd then + local lowerCmd = string.lower(cmd) + if not VALID_COMMANDS[lowerCmd] then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_COMMAND, + line = lineNum, + command = cmd, + message = "Unknown command: " .. cmd + }) end end - if conditionBlock then - if CleveRoids.Trim(conditionBlock) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_CONDITIONAL, - line = lineNum, - message = "Empty conditional block []" - }) - else - -- Check for invalid @ target syntax - local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") - if target and not string.find(target, "^@[a-z]+%d*") then - table.insert(errors, { - type = ERROR_TYPES.INVALID_TARGET, - line = lineNum, - message = "Invalid target: " .. target - }) - end + -- Check brackets + local bracketsOk, bracketError = checkBrackets(line) + if not bracketsOk then + table.insert(localErrors, { + type = ERROR_TYPES.MISMATCHED_BRACKETS, + line = lineNum, + message = bracketError or "Bracket mismatch" + }) + end - -- Parse individual conditionals - for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do - if condGroup ~= "" and condGroup ~= target then - local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") - local cond = string.lower(CleveRoids.Trim(parts[1] or "")) - local args = CleveRoids.Trim(parts[2] or "") + -- Check quotes + local quotesOk, quoteError = checkQuotes(line) + if not quotesOk then + table.insert(localErrors, { + type = ERROR_TYPES.MALFORMED_QUOTES, + line = lineNum, + message = quoteError or "Quote mismatch" + }) + end - if cond ~= "" then - -- Validate the conditional - local condErrors = validateConditional(cond, args, nil) - for _, err in condErrors do - err.line = lineNum - table.insert(errors, err) + -- Split by semicolons to handle multiple actions per line + local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") + if not actions then + return localErrors + end + + for _, actionPart in ipairs(actions) do + actionPart = safeTrim(actionPart) + if actionPart ~= "" and safeStringSub(actionPart, 1, 1) ~= "/" then + actionPart = "/" .. actionPart -- Add leading slash if missing after split + end + + -- Parse conditionals if present - use non-greedy match + local condStart = safeStringFind(actionPart, "%[") + local condEnd = nil + local conditionBlock = nil + + if condStart then + -- Find matching closing bracket + local depth = 0 + local inQuotes = false + local len = safeStringLen(actionPart) + + for i = condStart, len do + local char = safeStringSub(actionPart, i, i) + if not char or char == "" then break end + + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + depth = depth + 1 + elseif char == "]" then + depth = depth - 1 + if depth == 0 then + condEnd = i + conditionBlock = safeStringSub(actionPart, condStart + 1, i - 1) + break end end end end end - -- Check for action after conditionals - local afterCond = string.sub(actionPart, (condEnd or 0) + 1) - local _, _, action = string.find(afterCond, "^%s*[!~?]?(.+)") - if not action or CleveRoids.Trim(action) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_ACTION, - line = lineNum, - message = "Conditional has no action" - }) + if conditionBlock then + if safeTrim(conditionBlock) == "" then + table.insert(localErrors, { + type = ERROR_TYPES.EMPTY_CONDITIONAL, + line = lineNum, + message = "Empty conditional block []" + }) + else + -- Check for invalid @ target syntax + local _, _, target = safeStringFind(conditionBlock, "(@[^%s,]+)") + if target and not safeStringFind(target, "^@[a-z]+%d*") then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + local condGroups = CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) + if condGroups then + for _, condGroup in condGroups do + if condGroup ~= "" and condGroup ~= target then + local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") + if parts then + local cond = string.lower(safeTrim(parts[1] or "")) + local args = safeTrim(parts[2] or "") + + if cond ~= "" then + -- Validate the conditional + local condErrors = validateConditional(cond, args, nil) + for _, err in condErrors do + err.line = lineNum + table.insert(localErrors, err) + end + end + end + end + end + end + end + + -- Check for action after conditionals + local afterCond = safeStringSub(actionPart, (condEnd or 0) + 1) + local _, _, action = safeStringFind(afterCond, "^%s*[!~?]?(.+)") + if not action or safeTrim(action) == "" then + table.insert(localErrors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end end end + + return localErrors + end) + + if success and result then + return result + elseif not success then + -- An error occurred during validation + return {{ + type = "VALIDATION_ERROR", + line = lineNum, + message = "Internal error validating line: " .. tostring(result) + }} end return errors @@ -369,35 +438,64 @@ end -- Validate an entire macro function CleveRoids.ValidateMacro(macroName) - local errors = {} - local macroID = GetMacroIndexByName(macroName) - - if not macroID or macroID == 0 then - return {{ - type = "ERROR", - message = "Macro not found: " .. tostring(macroName) - }} - end - - local name, texture, body = GetMacroInfo(macroID) - if not body or body == "" then - return {{ - type = "ERROR", - message = "Macro is empty" - }} - end - - -- Split into lines - local lines = CleveRoids.splitString(body, "\n") - - for lineNum, line in ipairs(lines) do - local lineErrors = validateLine(line, lineNum) - for _, err in lineErrors do - table.insert(errors, err) + -- Wrap entire function in pcall for safety + local success, result = pcall(function() + local errors = {} + + if not macroName or macroName == "" then + return {{ + type = "ERROR", + message = "No macro name provided" + }} end - end + + local macroID = GetMacroIndexByName(macroName) - return errors + if not macroID or macroID == 0 then + return {{ + type = "ERROR", + message = "Macro not found: " .. tostring(macroName) + }} + end + + local name, texture, body = GetMacroInfo(macroID) + if not body or body == "" then + return {{ + type = "ERROR", + message = "Macro is empty" + }} + end + + -- Split into lines + local lines = CleveRoids.splitString(body, "\n") + if not lines then + return {{ + type = "ERROR", + message = "Failed to parse macro body" + }} + end + + for lineNum, line in ipairs(lines) do + local lineErrors = validateLine(line, lineNum) + if lineErrors then + for _, err in lineErrors do + table.insert(errors, err) + end + end + end + + return errors + end) + + if success then + return result + else + -- Return a safe error message if validation itself fails + return {{ + type = "CRITICAL_ERROR", + message = "Critical error validating macro: " .. tostring(result) + }} + end end -- Validate all macros @@ -405,11 +503,19 @@ function CleveRoids.ValidateAllMacros() local results = {} local totalErrors = 0 - local numMacros = GetNumMacros() + local success, numMacros = pcall(GetNumMacros) + if not success or not numMacros then + return results, 0 + end + for i = 1, numMacros do - local name = GetMacroInfo(i) - if name then + -- Wrap each macro validation in pcall so one bad macro doesn't stop all validation + local nameSuccess, name = pcall(GetMacroInfo, i) + + if nameSuccess and name and name ~= "" then + -- Validate this macro local errors = CleveRoids.ValidateMacro(name) + if errors and table.getn(errors) > 0 then results[name] = errors totalErrors = totalErrors + table.getn(errors) @@ -422,7 +528,12 @@ end -- Print errors for a macro function CleveRoids.PrintMacroErrors(macroName) - local errors = CleveRoids.ValidateMacro(macroName) + local success, errors = pcall(CleveRoids.ValidateMacro, macroName) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Failed to validate macro '" .. tostring(macroName) .. "': " .. tostring(errors)) + return + end if not errors or table.getn(errors) == 0 then CleveRoids.Print("|cff00ff00✓|r Macro '" .. macroName .. "' has no syntax errors") @@ -432,31 +543,45 @@ function CleveRoids.PrintMacroErrors(macroName) CleveRoids.Print("|cffff0000✗|r Macro '" .. macroName .. "' has " .. table.getn(errors) .. " error(s):") for _, err in errors do - local line = err.line and ("Line " .. err.line .. ": ") or "" - local msg = "|cffffaa00" .. line .. "|r" .. err.message - DEFAULT_CHAT_FRAME:AddMessage(" " .. msg) + if err and err.message then + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = "|cffffaa00" .. line .. "|r" .. err.message + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, " " .. msg) + end end end -- Print all macro errors function CleveRoids.PrintAllMacroErrors() - local results, totalErrors = CleveRoids.ValidateAllMacros() + local success, results, totalErrors = pcall(CleveRoids.ValidateAllMacros) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Failed to validate macros: " .. tostring(results)) + return + end if totalErrors == 0 then CleveRoids.Print("|cff00ff00✓|r All macros are error-free!") return end - CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. table.getn(results) .. " macro(s):|r") + local macroCount = 0 + for _ in pairs(results) do macroCount = macroCount + 1 end + + CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. macroCount .. " macro(s):|r") for macroName, errors in pairs(results) do - DEFAULT_CHAT_FRAME:AddMessage(" ") - DEFAULT_CHAT_FRAME:AddMessage("|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") + if macroName and errors then + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, " ") + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, "|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") - for _, err in errors do - local line = err.line and ("Line " .. err.line .. ": ") or "" - local msg = " |cffffaa00" .. line .. "|r" .. err.message - DEFAULT_CHAT_FRAME:AddMessage(msg) + for _, err in errors do + if err and err.message then + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = " |cffffaa00" .. line .. "|r" .. err.message + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, msg) + end + end end end end @@ -464,12 +589,18 @@ end -- Slash command SLASH_MACROCHECK1 = "/macrocheck" SlashCmdList.MACROCHECK = function(msg) - msg = CleveRoids.Trim(msg or "") + local success, result = pcall(function() + msg = safeTrim(msg or "") - if msg == "" or msg == "all" then - CleveRoids.PrintAllMacroErrors() - else - CleveRoids.PrintMacroErrors(msg) + if msg == "" or msg == "all" then + CleveRoids.PrintAllMacroErrors() + else + CleveRoids.PrintMacroErrors(msg) + end + end) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Macro check failed: " .. tostring(result)) end end