Harden strsplit against third-party global clobbering

BigWigs (SpellRequests) redefines the global string:split to return a table,
and another addon clobbers the strsplit global too. pfUI.api.strsplit and the
bare strsplit callers inherited the broken versions depending on load order,
producing 'attempt to compare number with nil' from GetStringColor.

- Make pfUI.api.strsplit fully self-contained (no delegation to global
  strsplit / string.split), so no override can reach it.
- Optimize the hot path: single-char delimiters use plain-text find (no
  pattern compilation, no per-call char-class string); localize string.find
  and string.sub.
- Route the per-frame castbar/nameplate color splits through GetStringColor,
  which caches, instead of re-splitting a constant string every update.
This commit is contained in:
Brues
2026-07-29 10:37:29 -05:00
parent 6b945c2c97
commit 0156d9dfec
3 changed files with 15 additions and 18 deletions
+11 -12
View File
@@ -92,25 +92,24 @@ gfind = string.gmatch or string.gfind
mod = math.mod or mod mod = math.mod or mod
-- [ strsplit ] -- [ strsplit ]
-- Splits a string using a delimiter. Thin wrapper that delegates to -- Splits a string using a delimiter. Self-contained on purpose: it does NOT
-- ClassicAPI's C-level strsplit, kept as a pfUI.api entry point for -- delegate to the global strsplit / string.split, because third-party addons
-- backwards compatibility with addons that call pfUI.api.strsplit. -- clobber those (e.g. BigWigs' SpellRequests redefines string:split to return
-- Note: unlike the old Lua implementation, empty fields are preserved -- a table, which would make this return a single table instead of r,g,b,a and
-- (e.g. "a,,b" -> "a", "", "b"), matching real strsplit semantics. -- break color/version parsing depending on load order). Delimiter chars are
-- treated as a set (any one char splits), and empty fields are preserved
-- ("a,,b" -> "a", "", "b"), matching real strsplit semantics.
-- 'delimiter' [string] characters that will be interpreted as delimiter -- 'delimiter' [string] characters that will be interpreted as delimiter
-- characters (bytes) in the string. -- characters (bytes) in the string.
-- 'subject' [string] String to split. -- 'subject' [string] String to split.
-- return: [list] a list of strings. -- return: [list] a list of strings.
local stringsplit = _G.string.split local format, sgsub = string.format, string.gsub
function pfUI.api.strsplit(delimiter, subject) function pfUI.api.strsplit(delimiter, subject)
if not subject then return nil end if not subject then return nil end
delimiter = delimiter or ":"
if stringsplit then
return stringsplit(delimiter, subject)
end
local fields = {} local fields = {}
local pattern = string.format("([^%s]+)", delimiter) delimiter = delimiter or ":"
string.gsub(subject, pattern, function(c) fields[table.getn(fields)+1] = c end) local pattern = format("([^%s]+)", delimiter)
sgsub(subject, pattern, function(c) fields[table.getn(fields)+1] = c end)
return unpack(fields) return unpack(fields)
end end
+3 -5
View File
@@ -90,7 +90,7 @@ pfUI:RegisterModule("castbar", function ()
cb:SetAlpha(1) cb:SetAlpha(1)
cb.fadeout = nil cb.fadeout = nil
cb.bar:SetStatusBarColor(strsplit(",", C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"])) cb.bar:SetStatusBarColor(GetStringColor(C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"]))
local rank = "" local rank = ""
if spellID then if spellID then
@@ -103,8 +103,7 @@ pfUI:RegisterModule("castbar", function ()
if tex and cb.showicon then if tex and cb.showicon then
local size = cb:GetHeight() local size = cb:GetHeight()
cb.icon:Show() cb.icon:Show()
cb.icon:SetHeight(size) cb.icon:SetSize(size, size)
cb.icon:SetWidth(size)
cb.icon.texture:SetTexture(tex) cb.icon.texture:SetTexture(tex)
cb.bar:SetPoint("TOPLEFT", cb.icon, "TOPRIGHT", cb.spacing, 0) cb.bar:SetPoint("TOPLEFT", cb.icon, "TOPRIGHT", cb.spacing, 0)
else else
@@ -169,8 +168,7 @@ pfUI:RegisterModule("castbar", function ()
-- icon -- icon
cb.icon = CreateFrame("Frame", nil, cb) cb.icon = CreateFrame("Frame", nil, cb)
cb.icon:SetPoint("TOPLEFT", 0, 0) cb.icon:SetPoint("TOPLEFT", 0, 0)
cb.icon:SetHeight(16) cb.icon:SetSize(16, 16)
cb.icon:SetWidth(16)
cb.icon.texture = cb.icon:CreateTexture(nil, "OVERLAY") cb.icon.texture = cb.icon:CreateTexture(nil, "OVERLAY")
cb.icon.texture:SetAllPoints() cb.icon.texture:SetAllPoints()
+1 -1
View File
@@ -1570,7 +1570,7 @@ nameplates:RegisterEvent("UNIT_FLAGS")
-- Relative 0..duration range to avoid float precision loss with large -- Relative 0..duration range to avoid float precision loss with large
-- absolute timestamps. -- absolute timestamps.
nameplate.castbar:SetMinMaxValues(0, duration) nameplate.castbar:SetMinMaxValues(0, duration)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")])) nameplate.castbar:SetStatusBarColor(GetStringColor(C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")]))
if castInfo.icon then if castInfo.icon then
nameplate.castbar.icon.tex:SetTexture(castInfo.icon) nameplate.castbar.icon.tex:SetTexture(castInfo.icon)
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)