Skip to content

Commit 5c063ad

Browse files
committed
lua: add strsplit; bind lua_checkstack
Backport the WoW global strsplit(sep, str [, pieces]) to 1.12, ported from 3.3.5's strsplit (FUN_00816a60): split str on any character in sep, returning the pieces as multiple values; pieces>0 caps the count with the unsplit remainder as the final piece (0/omitted = unlimited, 1 = whole string). Consecutive/trailing delimiters yield empty pieces, matching retail. Two deliberate changes from the 3.3.5 source: - Skip its lua_settop(L, 0). Popping the args un-roots the source string, so a GC step during a pushlstring could free the buffer we're still scanning. Leaving the args on the stack keeps the source GC-rooted; Lua returns the top N (the pieces) regardless of the args below. - Bind lua_checkstack (0x006F2F30, __fastcall(L, size) -> int, found via its cmp ...,0x800 LUA_MAXCSTACK test) as Game::Lua::CheckStack and guard every push with it, erroring "strsplit(): Stack overflow" like 3.3.5 on genuine exhaustion (strsplit pushes an unbounded number of results). Reusable for any future multi-return C function. Registered on both Lua states. Verified in-game across basic split, pieces cap, multi-char separator set, empty middle piece, and empty-separator cases.
1 parent 2bd3328 commit 5c063ad

6 files changed

Lines changed: 104 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
4545
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
4646
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
4747
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
48-
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `table.wipe` |
48+
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `strsplit`, `table.wipe` |
4949
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
5050
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
5151
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |

docs/API.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ build instructions.
282282
- [`select(index, ...)`](#selectindex-)
283283
- [`table.wipe(t)`](#tablewipet)
284284
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
285+
- [`strsplit(sep, str [, pieces])`](#strsplitsep-str--pieces)
285286
- [`math.fmod(x, y)`](#mathfmodx-y)
286287
- [`coroutine.create(fn)`](#coroutinecreatefn)
287288
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
@@ -6839,6 +6840,31 @@ for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / cc
68396840
> a hot VM-core function. This is the long-standing vanilla 1.12 constraint —
68406841
> always write `string.match(s, p)`, never `s:match(p)`.
68416842

6843+
### `strsplit(sep, str [, pieces])`
6844+
6845+
The WoW global (backported from 3.3.5), splits `str` on **any** character in
6846+
`sep` and returns the pieces as multiple values.
6847+
6848+
- `sep` — a set of delimiter *characters* (not a pattern); each character is
6849+
a delimiter. `","` splits on commas; `" -"` splits on spaces and dashes.
6850+
- `pieces` (optional) — caps the number of results. After `pieces - 1`
6851+
splits the rest of the string (delimiters and all) is returned as the final
6852+
piece. `0` or omitted = unlimited. `1` returns the whole string unsplit.
6853+
6854+
Consecutive/trailing delimiters produce empty pieces, matching retail.
6855+
6856+
```lua
6857+
strsplit(",", "a,b,c") -- "a", "b", "c"
6858+
strsplit(",", "a,b,c,d", 2) -- "a", "b,c,d" (capped at 2)
6859+
strsplit(" -", "a b-c") -- "a", "b", "c" (space OR dash)
6860+
strsplit(",", "a,,b") -- "a", "", "b" (empty middle piece)
6861+
local zone, x, y = strsplit(":", "Durotar:52:38")
6862+
```
6863+
6864+
Errors `strsplit(): Stack overflow` if a string splits into more pieces than
6865+
the Lua stack can grow to hold (`lua_checkstack` guards every push) — the
6866+
same guard and message 3.3.5 uses.
6867+
68426868
### `math.fmod(x, y)`
68436869

68446870
The floating-point remainder of `x / y` (the quotient truncated toward zero),

src/Game.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ namespace Lua {
5757
F(Resume, lua_resume, LUA_RESUME) \
5858
F(Yield, lua_yield, LUA_YIELD) \
5959
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
60-
F(SetN, luaL_setn, LUAL_SETN)
60+
F(SetN, luaL_setn, LUAL_SETN) \
61+
F(CheckStack, lua_checkstack, LUA_CHECK_STACK)
6162

6263
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
6364
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);

src/Game.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ using lua_resume_t = int(__fastcall *)(void *L, int nargs);
100100
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
101101
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
102102
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
103+
using lua_checkstack_t = int(__fastcall *)(void *L, int size);
103104

104105
extern const lua_isnumber_t IsNumber;
105106
extern const lua_isstring_t IsString;
@@ -142,6 +143,7 @@ extern const lua_resume_t Resume;
142143
extern const lua_yield_t Yield;
143144
extern const luaL_argerror_t ArgError;
144145
extern const luaL_setn_t SetN;
146+
extern const lua_checkstack_t CheckStack;
145147

146148
// Returns the global `lua_State *` (read on demand from the engine's global).
147149
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.

src/Offsets.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3796,6 +3796,14 @@ enum Offsets {
37963796
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
37973797
// appends past the wiped slots.
37983798
LUAL_SETN = 0x6F4EA0,
3799+
// `lua_checkstack(L, n)` — `int __fastcall(L /*ecx*/, n /*edx*/)`.
3800+
// Ensures room for `n` more stack values, growing if needed; returns 0
3801+
// (without growing) when `(top-base)/16 + n` would exceed LUA_MAXCSTACK
3802+
// (0x800 = 2048), 1 otherwise. Verified by the `cmp reg, 0x800` overflow
3803+
// test at 0x006F2F43. Needed by any C function that pushes an unbounded
3804+
// number of results (e.g. `strsplit`) — pushing past the ~20-slot
3805+
// C-call guarantee without growing corrupts the stack.
3806+
LUA_CHECK_STACK = 0x006F2F30,
37993807
// `str_find` — the Lua 5.0 string-library `string.find` C function
38003808
// (`int __fastcall(void *L)`), entry in the strlib luaL_reg table at
38013809
// `0x00822dd0`. Returns `1` (nil pushed, no match), `2` (start, end),

src/baselib/StringLib.cpp

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@
1111
// You should have received a copy of the GNU General Public License along with
1212
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
1313

14-
// Lua 5.1 string-library additions that 1.12's Lua 5.0 is missing:
14+
// String helpers that 1.12's Lua 5.0 is missing:
1515
//
16-
// - `string.match(s, pattern [, init])` — first-match extraction.
17-
// - `string.gmatch(s, pattern)` — match iterator.
16+
// - `string.match(s, pattern [, init])` — first-match extraction (5.1).
17+
// - `string.gmatch(s, pattern)` — match iterator (5.1).
18+
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
1819
//
19-
// Both are the same underlying pattern engine as functions 5.0 already ships:
20+
// The two `string.*` ones reuse pattern machinery 5.0 already ships:
2021
// - `match` is `find` returning captures / the whole match instead of the
2122
// start/end indices — so we call the engine's `str_find` and transform.
2223
// - `gmatch` is exactly 5.0's `string.gfind` (renamed in 5.1); we register
2324
// it as a direct alias of the engine's `gfind` C function.
25+
// `strsplit` is a hand-rolled port of 3.3.5's `strsplit` (see below).
2426
//
2527
// NOTE: only the `string.foo(s, ...)` call form works — NOT the
2628
// `("x"):foo(...)` method sugar. WoW's Lua VM strips type-metatables for
@@ -83,9 +85,68 @@ int __fastcall Script_string_match(void *L) {
8385
const auto Script_string_gmatch =
8486
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
8587

88+
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
89+
// `string.split`), split `str` on ANY character in `sep` and return the
90+
// pieces as multiple values. `pieces > 0` caps the result count, with the
91+
// unsplit remainder as the final piece; `0` / omitted = unlimited. Ported
92+
// from 3.3.5's `strsplit` (FUN_00816a60) with one deliberate change: we do
93+
// NOT `lua_settop(L, 0)` up front. Popping the args would leave the source
94+
// string unreferenced, so a GC step triggered by a `pushlstring` could free
95+
// the very buffer we're still scanning. Keeping the args on the stack keeps
96+
// the source GC-rooted; Lua returns the top N values (our pieces) regardless
97+
// of the args sitting below them. Each push is guarded by `lua_checkstack`
98+
// (this pushes an unbounded number of results), erroring like 3.3.5 on
99+
// genuine stack exhaustion.
100+
int __fastcall Script_strsplit(void *L) {
101+
const char *sep = Game::Lua::ToString(L, 1);
102+
const char *str = Game::Lua::ToString(L, 2);
103+
if (sep == nullptr || str == nullptr) {
104+
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
105+
return 0; // unreachable
106+
}
107+
const int pieces =
108+
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
109+
110+
const char *segStart = str;
111+
int count = 0;
112+
// Only scan for separators while unlimited (pieces == 0) or the cap
113+
// hasn't been reached (pieces > 1). pieces == 1 (or <= 0 but non-zero)
114+
// yields the whole string as a single piece — matches 3.3.5.
115+
if (pieces == 0 || pieces > 1) {
116+
for (const char *p = str; *p != '\0'; ++p) {
117+
bool isSep = false;
118+
for (const char *s = sep; *s != '\0'; ++s) {
119+
if (*p == *s) {
120+
isSep = true;
121+
break;
122+
}
123+
}
124+
if (!isSep)
125+
continue;
126+
++count;
127+
if (Game::Lua::CheckStack(L, count) == 0) {
128+
Game::Lua::Error(L, "strsplit(): Stack overflow");
129+
return 0; // unreachable
130+
}
131+
Game::Lua::PushLString(L, segStart,
132+
static_cast<unsigned int>(p - segStart));
133+
segStart = p + 1;
134+
if (count == pieces - 1)
135+
break; // cap hit; remainder becomes the final piece
136+
}
137+
}
138+
if (Game::Lua::CheckStack(L, count + 1) == 0) {
139+
Game::Lua::Error(L, "strsplit(): Stack overflow");
140+
return 0; // unreachable
141+
}
142+
Game::Lua::PushString(L, segStart); // final piece (remainder to end)
143+
return count + 1;
144+
}
145+
86146
void RegisterFns() {
87147
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
88148
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
149+
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
89150
}
90151

91152
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction

0 commit comments

Comments
 (0)