tinymux/mux/modules/engine/funceval2.cpp
Stephen Dennis 8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`.  Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.

The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.

New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.

The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper).  const_cast budget: zero
added.

Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW).  delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup.  After any signature flip, grep the
old spelling.

Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00

3104 lines
84 KiB
C++

/*! \file funceval2.cpp
* \brief MUX function handlers.
*
* This file began as a place to put function handlers ported from other
* MU* servers, but has also become home to miscellaneous new functions.
* These handlers include side-effect functions, comsys / mail functions,
* ansi functions, zone functions, encrypt / decrypt, random functions,
* some text-formatting and list-munging functions, deprecated stack
* functions, regexp functions, etc.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "ast.h"
#include <memory>
#include <vector>
extern "C" {
#include "color_ops.h"
}
#define PCRE2_CODE_UNIT_WIDTH 8
#include <pcre2.h>
/* ---------------------------------------------------------------------------
* fun_grab: a combination of extract() and match(), sortof. We grab the
* single element that we match.
*
* grab(Test:1 Ack:2 Foof:3,*:2) => Ack:2
* grab(Test-1+Ack-2+Foof-3,*o*,+) => Foof-3
* Borrowed from PennMUSH 1.50
*/
FUNCTION(fun_grab)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
// Walk the wordstring, until we find the word we want.
//
LBuf scList = LBuf_Src("fun_grab.list");
UTF8 *s = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
do
{
UTF8 *r = split_token(&s, sep);
mudstate.wild_invk_ctr = 0;
if (quick_wild(fargs[1], r))
{
safe_str(r, buff, bufc);
return;
}
} while (s);
}
FUNCTION(fun_graball)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
bool bFirst = true;
LBuf scList = LBuf_Src("fun_graball.list");
UTF8 *s = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
do
{
UTF8 *r = split_token(&s, sep);
mudstate.wild_invk_ctr = 0;
if (quick_wild(fargs[1], r))
{
if (!bFirst)
{
print_sep(osep, buff, bufc);
}
else
{
bFirst = false;
}
safe_str(r, buff, bufc);
}
} while (s);
}
/* ---------------------------------------------------------------------------
* fun_scramble: randomizes the letters in a string.
* Borrowed from PennMUSH 1.50
*/
FUNCTION(fun_scramble)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const unsigned char *p = reinterpret_cast<const unsigned char *>(fargs[0]);
size_t slen = strlen(reinterpret_cast<const char *>(p));
size_t nClusters = co_cluster_count(p, slen);
if (nClusters < 2)
{
safe_str(fargs[0], buff, bufc);
return;
}
// #1110: heap-allocate index / cluster scratch (was LBUF-scale stack).
//
std::vector<LBUF_OFFSET> indices(nClusters);
for (size_t i = 0; i < nClusters; i++)
{
indices[i] = static_cast<LBUF_OFFSET>(i);
}
for (size_t i = nClusters - 1; i > 0; i--)
{
size_t j = static_cast<size_t>(RandomINT32(0, static_cast<int32_t>(i)));
LBUF_OFFSET tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
// Output clusters in shuffled order (#2045).
//
// This used to call co_mid_cluster() once per cluster. Each of those
// walks the whole string to find the requested index -- two
// co_cluster_advance() calls, each stripping colour into a 32 KB buffer
// and re-walking from the start -- so the cost was O(slen) per cluster
// and O(slen^2) for the loop. scramble() is CA_PUBLIC, so at LBUF
// length any player could occupy the single-threaded server for ~19s,
// and max_cmdsecs/lag_limit cannot preempt a call already running.
//
// One pass builds the whole boundary table instead; emitting is then a
// memcpy per cluster. co_cluster_offsets() reproduces co_mid_cluster()'s
// ranges exactly, colour included, so the output is unchanged.
//
std::vector<size_t> offsets(nClusters + 1);
const size_t nFound = co_cluster_offsets(p, slen, offsets.data(),
offsets.size());
// co_cluster_count() and co_cluster_offsets() walk the same way, so this
// should not fire; clamp rather than trust it, since indices[] was built
// from the first and is about to index the second.
const size_t nEmit = (nFound < nClusters) ? nFound : nClusters;
for (size_t i = 0; i < nEmit; i++)
{
const size_t iCluster = indices[i];
const size_t off = offsets[iCluster];
size_t cb = offsets[iCluster + 1] - off;
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (cb > nMax) cb = nMax;
memcpy(*bufc, p + off, cb);
*bufc += cb;
}
**bufc = '\0';
}
/* ---------------------------------------------------------------------------
* fun_shuffle: randomize order of words in a list.
* Borrowed from PennMUSH 1.50
*/
FUNCTION(fun_shuffle)
{
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(3, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
if (1 == sep.n && 1 == osep.n)
{
// Single-char delimiter: use co_words_count + co_extract.
//
size_t slen;
const unsigned char *p = reinterpret_cast<const unsigned char *>(
trim_space_sep_n(fargs[0], sep, &slen));
unsigned char delim = static_cast<unsigned char>(sep.str[0]);
unsigned char out_delim = static_cast<unsigned char>(osep.str[0]);
size_t n = co_words_count(p, slen, delim);
if (0 == n)
{
return;
}
// #1110: heap-allocate index / word scratch (was LBUF-scale stack).
//
std::vector<LBUF_OFFSET> indices(n);
for (size_t i = 0; i < n; i++)
{
indices[i] = static_cast<LBUF_OFFSET>(i);
}
for (size_t i = n - 1; i > 0; i--)
{
size_t j = static_cast<size_t>(RandomINT32(0, static_cast<int32_t>(i)));
LBUF_OFFSET tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
// Locate every word ONCE, then emit by index (#2057).
//
// This used to call co_extract(p, slen, indices[i] + 1, 1, ...) per
// word. co_extract addresses word i by scanning forward from the
// start of the string counting delimiters, so it is O(slen) whichever
// word is wanted -- and asking for every word in turn is O(slen^2).
// At LBUF length one shuffle() occupied the single-threaded server for
// ~3.4s, and shuffle() is CA_PUBLIC.
//
// Same defect and same repair as fun_scramble (#2045), which was
// re-walking with co_mid_cluster; the multi-char branch below already
// worked this way. co_split_words agrees with co_extract about what a
// word is -- delimiter compression for space, exact for everything
// else, colour codes staying with the word they precede -- and
// tests/color_ops' split_extract_parity suite is what proves it rather
// than assuming it.
//
std::vector<size_t> wstarts(n), wends(n);
size_t nFound = co_split_words(p, slen, &delim, 1,
wstarts.data(), wends.data(), n);
// indices[] was built from co_words_count and is about to index a
// table built by co_split_words. tests/color_ops' split_extract_parity
// suite asserts the two agree, so nFound == n and this clamp never
// bites.
//
// Note what it does and does not do, because the two are easy to swap.
// It bounds the LOOP COUNT, not the index. indices[] is a shuffled
// permutation over [0, n), so its first nEmit entries are nEmit
// DISTINCT indices -- not the indices [0, nFound). If the two
// functions ever did disagree, w >= nFound would still be reached.
//
// What keeps that in bounds is the ALLOCATION: the tables are sized n
// rather than nFound and are value-initialised, so an entry
// co_split_words never wrote reads 0 - 0 and emits an empty word.
// Size them to nFound -- the obvious tightening once someone notices
// they are oversized in exactly the case this clamp is for -- and this
// becomes an out-of-bounds read with the clamp still apparently in
// place.
const size_t nEmit = (nFound < n) ? nFound : n;
bool bFirst = true;
for (size_t i = 0; i < nEmit; i++)
{
if (!bFirst)
{
safe_chr(static_cast<UTF8>(out_delim), buff, bufc);
}
else
{
bFirst = false;
}
const size_t w = indices[i];
size_t nWord = wends[w] - wstarts[w];
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nWord > nMax) nWord = nMax;
memcpy(*bufc, p + wstarts[w], nWord);
*bufc += nWord;
}
**bufc = '\0';
}
else
{
// Multi-char delimiter: use co_split_words + Fisher-Yates.
// #1110: heap-allocate word-boundary tables (was ~512 KiB stack).
//
const unsigned char *pData = reinterpret_cast<const unsigned char *>(fargs[0]);
size_t nLen = strlen(reinterpret_cast<const char *>(fargs[0]));
std::unique_ptr<size_t[]> wstarts(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
std::unique_ptr<size_t[]> wends(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
size_t nWords = co_split_words(pData, nLen,
reinterpret_cast<const unsigned char *>(sep.str),
sep.n, wstarts.get(), wends.get(), LBUF_SIZE);
/* Build index array and shuffle via Fisher-Yates. */
std::vector<LBUF_OFFSET> indices(nWords);
for (size_t j = 0; j < nWords; j++)
indices[j] = static_cast<LBUF_OFFSET>(j);
for (size_t j = nWords; j > 1; j--)
{
LBUF_OFFSET pick = static_cast<LBUF_OFFSET>(
RandomINT32(0, static_cast<int32_t>(j) - 1));
LBUF_OFFSET tmp = indices[j-1];
indices[j-1] = indices[pick];
indices[pick] = tmp;
}
bool bFirst = true;
for (size_t j = 0; j < nWords; j++)
{
if (bFirst)
{
bFirst = false;
}
else
{
print_sep(osep, buff, bufc);
}
LBUF_OFFSET w = indices[j];
size_t nb = wends[w] - wstarts[w];
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nb > nMax) nb = nMax;
memcpy(*bufc, pData + wstarts[w], nb);
*bufc += nb;
}
**bufc = '\0';
}
}
// pickrand -- choose a random item from a list.
//
FUNCTION(fun_pickrand)
{
SEP sep;
if ( nfargs == 0
|| fargs[0][0] == '\0'
|| !OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
size_t slen;
const UTF8 *s = trim_space_sep_n(fargs[0], sep, &slen);
if (0 == slen)
{
return;
}
if (1 == sep.n)
{
// Single-char delimiter: use co_words_count + co_extract.
//
const unsigned char *p = reinterpret_cast<const unsigned char *>(s);
unsigned char delim = static_cast<unsigned char>(sep.str[0]);
size_t n = co_words_count(p, slen, delim);
if (0 < n)
{
size_t w = static_cast<size_t>(RandomINT32(0, static_cast<int32_t>(n-1)));
std::vector<unsigned char> out(LBUF_SIZE);
size_t nOut = co_extract(out.data(), p, slen,
w + 1, 1, delim, delim);
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nOut > nMax) nOut = nMax;
memcpy(*bufc, out.data(), nOut);
*bufc += nOut;
**bufc = '\0';
}
}
else
{
// Multi-char delimiter: use co_split_words.
// #1110: heap-allocate word-boundary tables (was ~512 KiB stack).
//
const unsigned char *pData = reinterpret_cast<const unsigned char *>(s);
size_t nLen = slen;
std::unique_ptr<size_t[]> wstarts(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
std::unique_ptr<size_t[]> wends(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
size_t nWords = co_split_words(pData, nLen,
reinterpret_cast<const unsigned char *>(sep.str),
sep.n, wstarts.get(), wends.get(), LBUF_SIZE);
if (nWords > 0)
{
LBUF_OFFSET w = static_cast<LBUF_OFFSET>(
RandomINT32(0, static_cast<int32_t>(nWords) - 1));
size_t nb = wends[w] - wstarts[w];
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nb > nMax) nb = nMax;
memcpy(*bufc, pData + wstarts[w], nb);
*bufc += nb;
**bufc = '\0';
}
}
}
// sortby()
//
typedef struct
{
UTF8 *buff;;
dbref executor;
dbref caller;
dbref enactor;
int aflags;
} ucomp_context;
static int u_comp(ucomp_context *pctx, const void *s1, const void *s2)
{
if ( mudstate.func_invk_ctr > mudconf.func_invk_lim
|| mudstate.func_nest_lev > mudconf.func_nest_lim
|| alarm_clock.alarmed)
{
return 0;
}
if ((pctx->aflags & AF_NOEVAL) || NoEval(pctx->executor))
{
return 0;
}
const UTF8 *elems[2] = { T(s1), T(s2) };
LBuf tbuf = LBuf_Src("u_comp");
mux_strncpy(tbuf, pctx->buff, LBUF_SIZE-1);
LBuf result = LBuf_Src("u_comp");
UTF8 *bp = result;
// Use ast_exec (not mux_exec) to bypass the JIT compiler.
// The JIT does not support dynamically-provided cargs — it would
// use the outer caller's cargs instead of the comparison elements.
//
ast_exec(tbuf, LBUF_SIZE-1, result, &bp, pctx->executor, pctx->caller, pctx->enactor,
AttrTrace(pctx->aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), elems, 2);
*bp = '\0';
int64_t n = mux_atoi64(result);
return n;
}
inline int ucomp_bsearch(ucomp_context* pctx, void* arr[], int sz, void* ndl)
{
int l = 0;
int r = sz;
while (l < r)
{
int m = (l + r) >> 1;
if (m == sz)
{
return sz;
}
if (u_comp(pctx, ndl, arr[m]) < 0)
{
r = m;
}
else
{
l = m + 1;
}
}
return l;
}
static void mincomp_sort(ucomp_context* pctx, void* arr[], int sz)
{
if (sz <= 1)
{
return;
}
for (int i = 1; i < sz; i++)
{
void* t = arr[i];
int n = ucomp_bsearch(pctx, arr, i, t);
for (int j = i; j > n; j--)
{
arr[j] = arr[j-1];
}
arr[n] = t;
}
}
FUNCTION(fun_sortby)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = sep;
if (!OPTIONAL_DELIM(4, osep, DELIM_NULL|DELIM_CRLF|DELIM_STRING|DELIM_INIT))
{
return;
}
UTF8 *atext;
dbref thing;
dbref aowner;
int aflags;
if (!parse_and_get_attrib(executor, fargs, &atext, &thing, &aowner, &aflags, buff, bufc))
{
return;
}
ucomp_context ctx;
ctx.buff = alloc_lbuf("fun_sortby.ctx");
mux_strncpy(ctx.buff, atext, LBUF_SIZE-1);
ctx.executor = thing;
ctx.caller = executor;
ctx.enactor = enactor;
ctx.aflags = aflags;
LBuf list = LBuf_Src("fun_sortby");
list_copy_for_split(list, fargs[1]);
std::unique_ptr<UTF8*[]> ptrs(new UTF8*[LBUF_SIZE / 2]); // uninit (#2145): past-count reads are UB now, not nullptr
const int nptrs = list2arr(ptrs.get(), LBUF_SIZE / 2, list, sep);
if (nptrs > 1)
{
mincomp_sort(&ctx, reinterpret_cast<void**>(ptrs.get()), nptrs);
}
arr2list(ptrs.get(), nptrs, buff, bufc, osep);
free_lbuf(ctx.buff);
free_lbuf(atext);
}
// fun_last: Returns last word in a string. Borrowed from TinyMUSH 2.2.
//
FUNCTION(fun_last)
{
// If we are passed an empty arglist return a null string.
//
if (nfargs == 0)
{
return;
}
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
if (1 == sep.n)
{
// Single-char delimiter: use co_last.
// Trim leading/trailing delimiters for space (MUX compresses spaces).
//
size_t slen;
const UTF8 *bp = trim_space_sep_n(fargs[0], sep, &slen);
std::vector<unsigned char> out(LBUF_SIZE);
size_t nOut = co_last(out.data(),
reinterpret_cast<const unsigned char *>(bp), slen,
static_cast<unsigned char>(sep.str[0]));
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nOut > nMax) nOut = nMax;
memcpy(*bufc, out.data(), nOut);
*bufc += nOut;
**bufc = '\0';
}
else
{
// Multi-char delimiter: use co_split_words.
// #1110: heap-allocate word-boundary tables (was ~512 KiB stack).
//
const unsigned char *pData = reinterpret_cast<const unsigned char *>(fargs[0]);
size_t nLen = strlen(reinterpret_cast<const char *>(fargs[0]));
std::unique_ptr<size_t[]> ws(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
std::unique_ptr<size_t[]> we(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
size_t nWords = co_split_words(pData, nLen,
reinterpret_cast<const unsigned char *>(sep.str),
sep.n, ws.get(), we.get(), LBUF_SIZE);
if (nWords > 0)
{
size_t nb = we[nWords-1] - ws[nWords-1];
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nb > nMax) nb = nMax;
memcpy(*bufc, pData + ws[nWords-1], nb);
*bufc += nb;
**bufc = '\0';
}
}
}
/*
* ---------------------------------------------------------------------------
* fun_lrest: Returns all but the last word in a string.
*/
FUNCTION(fun_lrest)
{
// If we are passed an empty arglist return a null string.
//
if (nfargs == 0)
{
return;
}
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
if (1 == sep.n)
{
// Single-char delimiter: use co_extract for words 1..N-1.
//
size_t slen;
const unsigned char *p = reinterpret_cast<const unsigned char *>(
trim_space_sep_n(fargs[0], sep, &slen));
unsigned char delim = static_cast<unsigned char>(sep.str[0]);
size_t nWords = co_words_count(p, slen, delim);
if (nWords > 1)
{
std::vector<unsigned char> out(LBUF_SIZE);
size_t nOut = co_extract(out.data(), p, slen,
1, nWords - 1, delim, delim);
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nOut > nMax) nOut = nMax;
memcpy(*bufc, out.data(), nOut);
*bufc += nOut;
**bufc = '\0';
}
}
else
{
// Multi-char delimiter: use co_split_words.
// #1110: heap-allocate word-boundary tables (was ~512 KiB stack).
//
const unsigned char *pData = reinterpret_cast<const unsigned char *>(fargs[0]);
size_t nLen = strlen(reinterpret_cast<const char *>(fargs[0]));
std::unique_ptr<size_t[]> wstarts(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
std::unique_ptr<size_t[]> wends(new size_t[LBUF_SIZE]); // uninit (#2145): past-count reads are UB now, not nullptr
size_t nWords = co_split_words(pData, nLen,
reinterpret_cast<const unsigned char *>(sep.str),
sep.n, wstarts.get(), wends.get(), LBUF_SIZE);
if (nWords > 1)
{
size_t nb = wends[0] - wstarts[0];
size_t nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nb > nMax) nb = nMax;
memcpy(*bufc, pData + wstarts[0], nb);
*bufc += nb;
for (size_t i = 1; i < nWords - 1; i++)
{
print_sep(sep, buff, bufc);
nb = wends[i] - wstarts[i];
nMax = buff + (LBUF_SIZE-1) - *bufc;
if (nb > nMax) nb = nMax;
memcpy(*bufc, pData + wstarts[i], nb);
*bufc += nb;
}
**bufc = '\0';
}
}
}
// For an named object, or the executor, find the last created object
// (optionally qualified by type).
//
FUNCTION(fun_lastcreate)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(ncargs);
UNUSED_PARAMETER(cargs);
// Determine the target by name, or use the executor if no name is given.
//
dbref target = executor;
if ( 0 < nfargs
&& '\0' != fargs[0][0])
{
target = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(target))
{
safe_nomatch(buff, bufc);
return;
}
// Verify that the executor has access to the named object. Notice
// that an executor always has access to itself.
//
if ( !WizRoy(executor)
&& !Controls(executor, target))
{
safe_noperm(buff, bufc);
return;
}
}
// If a type is given, qualify the result.
//
int iObjectPosition = 4;
if ( 1 < nfargs
&& '\0' != fargs[1][0])
{
switch (fargs[1][0])
{
case 'R':
case 'r':
iObjectPosition = 0;
break;
case 'T':
case 't':
iObjectPosition = 1;
break;
case 'E':
case 'e':
iObjectPosition = 2;
break;
case 'P':
case 'p':
iObjectPosition = 3;
break;
}
}
int aowner;
int aflags;
// atr_get always allocates; free on every path (#1064).
LBuf newobject_string = LBuf_Adopt(atr_get("fun_lastcreate.2998", target,
A_NEWOBJS, &aowner, &aflags));
if ('\0' == newobject_string[0])
{
safe_str(S_("#-1"), buff, bufc);
return;
}
string_token st(newobject_string, T(" "));
int i;
UTF8* ptr;
for ( ptr = st.parse(), i = 0;
nullptr != ptr && i < 5;
ptr = st.parse(), i++)
{
if (i == iObjectPosition)
{
dbref jLastCreated = mux_atoi64(ptr);
safe_tprintf_str(buff, bufc, T("#%d"), jLastCreated);
break;
}
}
}
// Borrowed from TinyMUSH 2.2
//
FUNCTION(fun_matchall)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
int wcount;
UTF8 *r, *s, *old, tbuf[I32BUF_SIZE];
old = *bufc;
// Check each word individually, returning the word number of all that
// match. If none match, return 0.
//
wcount = 1;
LBuf scList = LBuf_Src("fun_matchall.list");
s = trim_space_sep(list_copy_for_split(scList, fargs[0]), sep);
do
{
r = split_token(&s, sep);
mudstate.wild_invk_ctr = 0;
if (quick_wild(fargs[1], r))
{
mux_ltoa(wcount, tbuf);
if (old != *bufc)
{
safe_chr(' ', buff, bufc);
}
safe_str(tbuf, buff, bufc);
}
wcount++;
} while (s);
if (*bufc == old)
{
safe_chr('0', buff, bufc);
}
}
// ---------------------------------------------------------------------------
// fun_ports: Returns a list of ports for a user.
// Borrowed from TinyMUSH 2.2
//
FUNCTION(fun_ports)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref target = lookup_player(executor, fargs[0], true);
if (Good_obj(target))
{
if (target == executor || Wizard(executor))
{
if (Connected(target))
{
make_portlist(executor, target, buff, bufc);
}
}
else
{
safe_noperm(buff, bufc);
}
}
else
{
safe_nomatch(buff, bufc);
}
}
/* ---------------------------------------------------------------------------
* fun_mix: Like map, but operates on up to ten lists simultaneously, passing
* the elements as %0 - %10.
* Borrowed from PennMUSH 1.50, upgraded by RhostMUSH.
*/
FUNCTION(fun_mix)
{
// Check to see if we have an appropriate number of arguments.
// If there are more than three arguments, the last argument is
// ALWAYS assumed to be a delimiter.
//
SEP sep;
int lastn;
if (nfargs < 4)
{
sep.n = 1;
sep.str[0] = ' ';
sep.str[1] = '\0';
lastn = nfargs - 1;
}
else if (!OPTIONAL_DELIM(nfargs, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
else
{
lastn = nfargs - 2;
}
// Get the attribute. Check the permissions.
//
dbref thing;
UTF8 *atext;
dbref aowner;
int aflags;
if (!parse_and_get_attrib(executor, fargs, &atext, &thing, &aowner, &aflags, buff, bufc))
{
return;
}
// Process the lists, one element at a time.
//
int i;
int nwords = 0;
UTF8 *cp[NUM_ENV_VARS];
FargVec lists(fargs + 1, lastn);
for (i = 0; i < lastn; i++)
{
cp[i] = trim_space_sep(lists[i], sep);
int twords = countwords(cp[i], sep);
if (nwords < twords)
{
nwords = twords;
}
}
const UTF8 *os[NUM_ENV_VARS];
bool bFirst = true;
for ( int wc = 0;
wc < nwords
&& mudstate.func_invk_ctr < mudconf.func_invk_lim
&& !alarm_clock.alarmed;
wc++)
{
if (!bFirst)
{
print_sep(sep, buff, bufc);
}
else
{
bFirst = false;
}
for (i = 0; i < lastn; i++)
{
os[i] = split_token(&cp[i], sep);
if (nullptr == os[i])
{
os[i] = T("");
}
}
mux_exec(atext, LBUF_SIZE-1, buff, bufc, thing, executor, enactor,
AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL),
os, lastn);
}
free_lbuf(atext);
}
/* ---------------------------------------------------------------------------
* fun_step: A little like a fusion of iter() and mix(), it takes elements
* of a list X at a time and passes them into a single function as %0, %1,
* etc. step(<attribute>,<list>,<step size>,<delim>,<outdelim>)
*/
FUNCTION(fun_step)
{
int i;
SEP isep;
if (!OPTIONAL_DELIM(4, isep, DELIM_DFLT|DELIM_STRING))
{
return;
}
SEP osep = isep;
if (!OPTIONAL_DELIM(5, osep, DELIM_NULL|DELIM_CRLF|DELIM_INIT|DELIM_STRING))
{
return;
}
int64_t step_size = mux_atoi64(fargs[2]);
if ( step_size < 1
|| NUM_ENV_VARS < step_size)
{
notify(executor, M_("Illegal step size."));
return;
}
// Get attribute. Check permissions.
//
UTF8 *atext;
dbref thing;
dbref aowner;
int aflags;
if (!parse_and_get_attrib(executor, fargs, &atext, &thing, &aowner, &aflags, buff, bufc))
{
return;
}
LBuf scList = LBuf_Src("fun_step.list");
UTF8 *cp = trim_space_sep(list_copy_for_split(scList, fargs[1]), isep);
const UTF8 *os[NUM_ENV_VARS];
bool bFirst = true;
while ( cp
&& mudstate.func_invk_ctr < mudconf.func_invk_lim
&& !alarm_clock.alarmed)
{
if (!bFirst)
{
print_sep(osep, buff, bufc);
}
else
{
bFirst = false;
}
for (i = 0; cp && i < step_size; i++)
{
os[i] = split_token(&cp, isep);
}
// Evaluate the attribute as the object that owns it (executor =
// thing), matching map()/mix()/foreach(); previously it ran as the
// caller, so %!/me and permissions resolved to the wrong object.
//
mux_exec(atext, LBUF_SIZE-1, buff, bufc, thing, executor, enactor,
AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), os, i);
}
free_lbuf(atext);
}
/* ---------------------------------------------------------------------------
* fun_foreach: like map(), but it operates on a string, rather than on a list,
* calling a user-defined function for each character in the string.
* No delimiter is inserted between the results.
* Borrowed from TinyMUSH 2.2
*/
FUNCTION(fun_foreach)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if ( nfargs != 2
&& nfargs != 4)
{
safe_str(S_("#-1 FUNCTION (FOREACH) EXPECTS 2 OR 4 ARGUMENTS"), buff, bufc);
return;
}
UTF8 *atext;
dbref thing;
dbref aowner;
int aflags;
if (!parse_and_get_attrib(executor, fargs, &atext, &thing, &aowner, &aflags, buff, bufc))
{
return;
}
// Trim leading/trailing whitespace directly on the buffer.
//
const UTF8 *pStr = fargs[1];
while (mux_isspace(*pStr)) pStr++;
size_t nStr = strlen(reinterpret_cast<const char *>(pStr));
while (nStr > 0 && mux_isspace(pStr[nStr - 1])) nStr--;
UTF8 cbuf[5] = {'\0', '\0', '\0', '\0', '\0'};
const UTF8 *bp = cbuf;
size_t i = 0;
if ( 4 == nfargs
&& '\0' != fargs[2][0]
&& '\0' != fargs[3][0])
{
bool flag = false;
UTF8 prev = '\0';
while ( i < nStr
&& mudstate.func_invk_ctr < mudconf.func_invk_lim
&& !alarm_clock.alarmed)
{
// Determine code point length from UTF-8 lead byte.
//
unsigned char ch = pStr[i];
size_t nBytes;
if (ch < 0x80) nBytes = 1;
else if (ch < 0xE0) nBytes = 2;
else if (ch < 0xF0) nBytes = 3;
else nBytes = 4;
if (i + nBytes > nStr) break;
memcpy(cbuf, pStr + i, nBytes);
cbuf[nBytes] = '\0';
i += nBytes;
if (flag)
{
if ( cbuf[0] == *fargs[3]
&& prev != '\\'
&& prev != '%')
{
flag = false;
continue;
}
}
else
{
if ( cbuf[0] == *fargs[2]
&& prev != '\\'
&& prev != '%')
{
flag = true;
continue;
}
else
{
safe_copy_buf(cbuf, nBytes, buff, bufc);
continue;
}
}
mux_exec(atext, LBUF_SIZE-1, buff, bufc, thing, executor, enactor,
AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), &bp, 1);
prev = cbuf[0];
}
}
else
{
while ( i < nStr
&& mudstate.func_invk_ctr < mudconf.func_invk_lim
&& !alarm_clock.alarmed)
{
unsigned char ch = pStr[i];
size_t nBytes;
if (ch < 0x80) nBytes = 1;
else if (ch < 0xE0) nBytes = 2;
else if (ch < 0xF0) nBytes = 3;
else nBytes = 4;
if (i + nBytes > nStr) break;
memcpy(cbuf, pStr + i, nBytes);
cbuf[nBytes] = '\0';
mux_exec(atext, LBUF_SIZE-1, buff, bufc, thing, executor, enactor,
AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), &bp, 1);
i += nBytes;
}
}
free_lbuf(atext);
}
/* ---------------------------------------------------------------------------
* fun_munge: combines two lists in an arbitrary manner.
* Borrowed from TinyMUSH 2.2
* Hash table rewrite by Ian and Alierak.
*/
#if LBUF_SIZE < UINT16_MAX
typedef uint16_t NHASH;
#define ShiftHash(x) (x) >>= 16
#else
typedef uint32_t NHASH;
#define ShiftHash(x)
#endif
typedef struct munge_htab_rec
{
NHASH nHash; // partial hash value of this record's key
LBUF_OFFSET iNext; // index of next record in this hash chain
LBUF_OFFSET nKeyOffset; // offset of key string (incremented by 1),
// zero indicates empty record.
LBUF_OFFSET nValueOffset; // offset of value string
} munge_htab_rec;
FUNCTION(fun_munge)
{
SEP sep;
if (!OPTIONAL_DELIM(4, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
// Find our object and attribute.
//
UTF8 *atext;
dbref thing;
dbref aowner;
int aflags;
if (!parse_and_get_attrib(executor, fargs, &atext, &thing, &aowner, &aflags, buff, bufc))
{
return;
}
// Prepare data structures for a hash table that will map
// elements of list1 to corresponding elements of list2.
//
int nWords = countwords(fargs[1], sep);
if (0 == nWords)
{
free_lbuf(atext);
return;
}
std::vector<munge_htab_rec> htab(1 + 2 * nWords);
std::vector<uint16_t> tails(1 + nWords);
int iNext = 1 + nWords; // first unused hash slot past starting area
// Chop up the lists, converting them into a hash table that
// maps elements of list1 to corresponding elements of list2.
// The tokenizing walks and the offset-based lookups below both use
// private copies of the two lists (#2136).
//
LBuf keys = LBuf_Src("fun_munge.keys");
UTF8 *p1 = trim_space_sep(list_copy_for_split(keys, fargs[1]), sep);
LBuf vals = LBuf_Src("fun_munge.vals");
UTF8 *p2 = trim_space_sep(list_copy_for_split(vals, fargs[2]), sep);
UTF8 *pKey, *pValue;
for (pKey = split_token(&p1, sep), pValue = split_token(&p2, sep);
nullptr != pKey && nullptr != pValue;
pKey = split_token(&p1, sep), pValue = split_token(&p2, sep))
{
uint32_t nHash = munge_hash(pKey);
int nHashSlot = 1 + (nHash % nWords);
ShiftHash(nHash);
if (0 != tails[nHashSlot])
{
// there is already a hash chain starting in this slot,
// insert at the tail to preserve order.
nHashSlot = tails[nHashSlot] =
htab[tails[nHashSlot]].iNext = static_cast<LBUF_OFFSET>(iNext++);
}
else
{
tails[nHashSlot] = static_cast<LBUF_OFFSET>(nHashSlot);
}
htab[nHashSlot].nHash = static_cast<NHASH>(nHash);
htab[nHashSlot].nKeyOffset = static_cast<LBUF_OFFSET>(1 + (pKey - keys.get()));
htab[nHashSlot].nValueOffset = static_cast<LBUF_OFFSET>(pValue - vals.get());
}
if ( nullptr != pKey
|| nullptr != pValue)
{
safe_str(S_("#-1 LISTS MUST BE OF EQUAL SIZE"), buff, bufc);
free_lbuf(atext);
return;
}
// Call the u-function with the first list as %0.
//
LBuf rlist = LBuf_Src("fun_munge");
UTF8 *bp;
const UTF8 *uargs[2];
// The walks above tokenized private copies, so fargs[1] is still
// pristine and can be %0 directly (this replaces #2157's copy).
//
bp = rlist;
uargs[0] = fargs[1];
uargs[1] = sep.str;
mux_exec(atext, LBUF_SIZE-1, rlist, &bp, executor, caller, enactor,
AttrTrace(aflags, EV_STRIP_CURLY|EV_FCHECK|EV_EVAL), uargs, 2);
*bp = '\0';
free_lbuf(atext);
// Now that we have our result, put it back into array form.
// Translate its elements according to the mappings in our hash table.
//
bool bFirst = true;
bp = trim_space_sep(rlist, sep);
if ('\0' != *bp)
{
UTF8 *result;
for (result = split_token(&bp, sep);
nullptr != result;
result = split_token(&bp, sep))
{
uint32_t nHash = munge_hash(result);
int nHashSlot = 1 + (nHash % nWords);
ShiftHash(nHash);
while ( 0 != htab[nHashSlot].nKeyOffset
&& ( nHash != htab[nHashSlot].nHash
|| 0 != strcmp(reinterpret_cast<char *>(result),
reinterpret_cast<char *>(keys.get() +
htab[nHashSlot].nKeyOffset - 1))))
{
nHashSlot = htab[nHashSlot].iNext;
}
if (0 != htab[nHashSlot].nKeyOffset)
{
if (!bFirst)
{
print_sep(sep, buff, bufc);
}
else
{
bFirst = false;
}
safe_str(vals.get() + htab[nHashSlot].nValueOffset, buff, bufc);
// delete from the hash table
memcpy(&htab[nHashSlot], &htab[htab[nHashSlot].iNext],
sizeof(munge_htab_rec));
}
}
}
}
FUNCTION(fun_die)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int64_t n = mux_atoi64(fargs[0]);
int64_t die = mux_atoi64(fargs[1]);
if ( n == 0
|| die <= 0)
{
safe_chr('0', buff, bufc);
return;
}
if ( n < 1
|| LBUF_SIZE <= n)
{
safe_range(buff, bufc);
return;
}
if ( 3 <= nfargs
&& isTRUE(mux_atoi64(fargs[2])))
{
ITL pContext;
ItemToList_Init(&pContext, buff, bufc);
for (int count = 0; count < n; count++)
{
if (!ItemToList_AddInteger(&pContext, RandomINT32(1, die)))
{
break;
}
}
ItemToList_Final(&pContext);
return;
}
int total = 0;
for (int count = 0; count < n; count++)
{
total += RandomINT32(1, die);
}
safe_ltoa(total, buff, bufc);
}
FUNCTION(fun_lrand)
{
SEP sep;
if (!OPTIONAL_DELIM(4, sep, DELIM_NULL|DELIM_CRLF|DELIM_STRING))
{
return;
}
int64_t n_times = mux_atoi64(fargs[2]);
if (n_times < 1)
{
return;
}
if (n_times > LBUF_SIZE)
{
n_times = LBUF_SIZE;
}
int32_t iLower = mux_atoi64(fargs[0]);
int32_t iUpper = mux_atoi64(fargs[1]);
if (iLower <= iUpper)
{
for (int i = 0; i < n_times-1; i++)
{
int32_t val = RandomINT32(iLower, iUpper);
safe_ltoa(val, buff, bufc);
print_sep(sep, buff, bufc);
}
int32_t val = RandomINT32(iLower, iUpper);
safe_ltoa(val, buff, bufc);
}
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_lit)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Just returns the argument, literally.
//
safe_str(fargs[0], buff, bufc);
}
FUNCTION(fun_dumping)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
#if !defined(HAVE_WORKING_FORK)
safe_chr('0', buff, bufc);
#else // HAVE_WORKING_FORK
safe_bool(mudstate.dumping, buff, bufc);
#endif // HAVE_WORKING_FORK
}
static size_t mux_Pack0(int64_t val, int iRadix, UTF8 symbols[], UTF8 *buf)
{
UTF8 *p = buf;
// Handle sign.
//
if (val < 0)
{
*p++ = '-';
val = -val;
}
UTF8 *q = p;
while (val > iRadix-1)
{
int64_t iDiv = val / iRadix;
int64_t iTerm = val - iDiv * iRadix;
val = iDiv;
*p++ = symbols[iTerm];
}
*p++ = symbols[val];
size_t nLength = p - buf;
*p-- = '\0';
// The digits are in reverse order with a possible leading '-'
// if the value was negative. q points to the first digit,
// and p points to the last digit.
//
while (q < p)
{
// Swap characters are *p and *q
//
char temp = *p;
*p = *q;
*q = temp;
// Move p and first digit towards the middle.
//
--p;
++q;
// Stop when we reach or pass the middle.
//
}
return nLength;
}
// The following table contains 64 symbols, so this supports -a-
// radix-64 encoding. It is not however 'unix-to-unix' encoding.
// All of the following characters are valid for an attribute
// name, but not for the first character of an attribute name.
//
static UTF8 aRadix64[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz@$";
// These sets are for compatibility with PennMUSH.
//
static UTF8 aRadixPenn36[] =
"0123456789abcdefghijklmnopqrstuvwxyz";
static UTF8 aRadixPenn64[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static bool mux_Unpack(const UTF8 *p, int64_t &val, int iRadixFrom, int iRadixTo, bool fPennBehavior)
{
if (10 == iRadixFrom)
{
val = mux_atoi64(p);
return true;
}
bool fPlusSlash = false;
bool fNegativeNumbers = true;
UTF8 *symbols = aRadix64;
if (fPennBehavior)
{
fNegativeNumbers = iRadixFrom < 63 && iRadixTo < 63;
if (iRadixFrom <= 36)
{
symbols = aRadixPenn36;
}
else if (iRadixFrom < 64)
{
symbols = aRadixPenn64;
}
else
{
fPlusSlash = true;
symbols = aRadixPenn64;
}
}
// Build Table of valid characters.
//
UTF8 MatchTable[256];
memset(MatchTable, 0, sizeof(MatchTable));
for (int i = 0; i < iRadixFrom; i++)
{
MatchTable[static_cast<unsigned char>(symbols[i])] = static_cast<UTF8>(i + 1);
}
if (fPlusSlash)
{
MatchTable[static_cast<unsigned char>('+')] = static_cast<UTF8>(62 + 1);
MatchTable[static_cast<unsigned char>('/')] = static_cast<UTF8>(63 + 1);
}
// Leading whitespace
//
while (mux_isspace(*p))
{
p++;
}
// Possible sign
//
int LeadingCharacter = '\0';
if (fNegativeNumbers)
{
LeadingCharacter = *p;
if ( '-' == LeadingCharacter
|| '+' == LeadingCharacter)
{
p++;
}
}
// Validate that string contains only characters from the subset of permitted characters.
//
int c;
const UTF8 *q = p;
while ( '\0' != *q
&& !mux_isspace(*q))
{
c = *q;
if (0 == MatchTable[static_cast<unsigned int>(c)])
{
return false;
}
q++;
}
// Verify trailing spaces.
//
while ('\0' != *q)
{
c = *q;
if (!mux_isspace(c))
{
return false;
}
q++;
}
// Convert symbols
//
val = 0;
c = *p++;
for (int iValue = MatchTable[static_cast<unsigned int>(c)];
iValue;
iValue = MatchTable[static_cast<unsigned int>(c)])
{
val = iRadixFrom * val + iValue - 1;
c = *p++;
}
// Interpret sign
//
if ('-' == LeadingCharacter)
{
val = -val;
}
return true;
}
static void mux_Pack1(int64_t val, int iRadixTo, bool fPennBehavior, UTF8 *buff, UTF8 **bufc)
{
if (10 == iRadixTo)
{
safe_i64toa(val, buff, bufc);
}
else if ( val < 0
&& 63 <= iRadixTo)
{
safe_str(S_("#-1 NEGATIVE NUMBER IS NOT REPRESENTABLE IN OUTPUT RADIX"), buff, bufc);
}
else
{
UTF8 *symbols = aRadix64;
if (fPennBehavior)
{
if (iRadixTo <= 36)
{
symbols = aRadixPenn36;
}
else
{
symbols = aRadixPenn64;
}
}
UTF8 TempBuffer[76]; // 1 '-', 63 binary digits, 1 '\0', 11 for safety.
size_t nLength = mux_Pack0(val, iRadixTo, symbols, TempBuffer);
safe_copy_buf(TempBuffer, nLength, buff, bufc);
}
}
FUNCTION(fun_unpack)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Validate radix if present.
//
int iRadix = 64;
if (2 <= nfargs)
{
if ( !is_integer(fargs[1], nullptr)
|| (iRadix = mux_atoi64(fargs[1])) < 2
|| 64 < iRadix)
{
safe_str(S_("#-1 RADIX MUST BE A NUMBER BETWEEN 2 and 64"), buff, bufc);
return;
}
}
bool fPennBehavior = false;
if (3 <= nfargs)
{
fPennBehavior = xlate(fargs[2]);
}
int64_t val;
if (!mux_Unpack(fargs[0], val, iRadix, 10, fPennBehavior))
{
safe_str(S_("#-1 NUMBER IS NOT VALID FOR INPUT RADIX"), buff, bufc);
}
else
{
safe_i64toa(val, buff, bufc);
}
}
FUNCTION(fun_pack)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Validate the arguments are numeric.
//
if ( !is_integer(fargs[0], nullptr)
|| (2 <= nfargs && !is_integer(fargs[1], nullptr)))
{
safe_str(S_("#-1 ARGUMENTS MUST BE NUMBERS"), buff, bufc);
return;
}
int64_t val = mux_atoi64(fargs[0]);
// Validate that the radix is between 2 and 64.
//
int iRadix = 64;
if (2 <= nfargs)
{
iRadix = mux_atoi64(fargs[1]);
if ( iRadix < 2
|| 64 < iRadix)
{
safe_str(S_("#-1 RADIX MUST BE A NUMBER BETWEEN 2 and 64"), buff, bufc);
return;
}
}
bool fPennBehavior = false;
if (3 <= nfargs)
{
fPennBehavior = xlate(fargs[2]);
}
mux_Pack1(val, iRadix, fPennBehavior, buff, bufc);
}
FUNCTION(fun_baseconv)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Validate that input and output radix are integers.
//
if ( !is_integer(fargs[1], nullptr)
|| !is_integer(fargs[2], nullptr))
{
safe_str(S_("#-1 ARGUMENTS MUST BE NUMBERS"), buff, bufc);
return;
}
int64_t iRadixFrom = mux_atoi64(fargs[1]);
if ( iRadixFrom < 2
|| 64 < iRadixFrom)
{
safe_str(S_("#-1 INPUT RADIX MUST BE A NUMBER BETWEEN 2 and 64"), buff, bufc);
return;
}
int64_t iRadixTo = mux_atoi64(fargs[2]);
if ( iRadixTo < 2
|| 64 < iRadixTo)
{
safe_str(S_("#-1 OUTPUT RADIX MUST BE A NUMBER BETWEEN 2 and 64"), buff, bufc);
return;
}
int64_t val;
if (!mux_Unpack(fargs[0], val, iRadixFrom, iRadixTo, true))
{
safe_str(S_("#-1 NUMBER IS NOT VALID FOR INPUT RADIX"), buff, bufc);
}
else
{
mux_Pack1(val, iRadixTo, true, buff, bufc);
}
}
FUNCTION(fun_strcat)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int i;
for (i = 0; i < nfargs; i++)
{
safe_str(fargs[i], buff, bufc);
}
}
// grep() and grepi() code borrowed from PennMUSH 1.50
//
static UTF8 *grep_util(dbref player, dbref thing, const UTF8 *pattern, const UTF8 *lookfor, size_t len, bool insensitive)
{
// Returns a list of attributes which match <pattern> on <thing>
// whose contents have <lookfor>.
//
olist_push();
find_wild_attrs(player, thing, pattern, false, false, false);
BMH_State bmhs;
if (insensitive)
{
BMH_PrepareI(&bmhs, len, lookfor);
}
else
{
BMH_Prepare(&bmhs, len, lookfor);
}
UTF8 *tbuf1 = alloc_lbuf("grep_util");
UTF8 *bp = tbuf1;
dbref aowner;
int aflags;
for (int ca = olist_first(); ca != NOTHING && !alarm_clock.alarmed; ca = olist_next())
{
size_t nText;
UTF8 *attrib = atr_get_LEN(thing, ca, &aowner, &aflags, &nText);
size_t i;
bool bSucceeded;
if (insensitive)
{
bSucceeded = BMH_ExecuteI(&bmhs, &i, len, lookfor, nText, attrib);
}
else
{
bSucceeded = BMH_Execute(&bmhs, &i, len, lookfor, nText, attrib);
}
if (bSucceeded)
{
if (bp != tbuf1)
{
safe_chr(' ', tbuf1, &bp);
}
ATTR *ap = atr_num(ca);
const UTF8 *pName = T("(WARNING: Bad Attribute Number)");
if (ap)
{
pName = ap->name;
}
safe_str(pName, tbuf1, &bp);
}
free_lbuf(attrib);
}
*bp = '\0';
olist_pop();
return tbuf1;
}
static void grep_handler(UTF8 *buff, UTF8 **bufc, dbref executor,
const UTF8 * const fargs[], bool bCaseInsens)
{
dbref it = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(it))
{
safe_match_result(it, buff, bufc);
return;
}
if (!Examinable(executor, it))
{
safe_noperm(buff, bufc);
return;
}
// Make sure there's an attribute and a pattern
//
if (!fargs[1] || !*fargs[1])
{
safe_str(S_("#-1 NO SUCH ATTRIBUTE"), buff, bufc);
return;
}
if (!fargs[2] || !*fargs[2])
{
safe_str(S_("#-1 INVALID GREP PATTERN"), buff, bufc);
return;
}
UTF8 *tp = grep_util(executor, it, fargs[1], fargs[2], strlen(reinterpret_cast<const char *>(fargs[2])), bCaseInsens);
safe_str(tp, buff, bufc);
free_lbuf(tp);
}
FUNCTION(fun_grep)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
grep_handler(buff, bufc, executor, fargs, false);
}
FUNCTION(fun_grepi)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
grep_handler(buff, bufc, executor, fargs, true);
}
// regrep()/regrepi() — regex-based attribute value searching.
// Like grep()/grepi() but uses PCRE2 regular expressions instead of
// wildcard patterns.
//
static UTF8 *regrep_util(dbref player, dbref thing, const UTF8 *pattern,
const UTF8 *lookfor, bool insensitive)
{
olist_push();
find_wild_attrs(player, thing, pattern, false, false, false);
// Compile the regex.
//
int errcode;
PCRE2_SIZE erroffset;
uint32_t options = PCRE2_UTF;
if (insensitive) options |= PCRE2_CASELESS;
pcre2_code *re = pcre2_compile_8(lookfor, PCRE2_ZERO_TERMINATED,
options, &errcode, &erroffset, nullptr);
if (!re)
{
olist_pop();
UTF8 *tbuf1 = alloc_lbuf("regrep_util");
mux_strncpy(tbuf1, S_("#-1 REGEXP ERROR"), LBUF_SIZE-1);
return tbuf1;
}
pcre2_match_data *match_data =
pcre2_match_data_create_from_pattern(re, nullptr);
// #1113: null-check match_data like regmatch/regrab/regedit.
//
if (!match_data)
{
pcre2_code_free(re);
olist_pop();
UTF8 *tbuf1 = alloc_lbuf("regrep_util");
mux_strncpy(tbuf1, S_("#-1 REGEXP MATCH DATA ERROR"), LBUF_SIZE-1);
return tbuf1;
}
UTF8 *tbuf1 = alloc_lbuf("regrep_util");
UTF8 *bp = tbuf1;
dbref aowner;
int aflags;
for (int ca = olist_first(); ca != NOTHING && !alarm_clock.alarmed;
ca = olist_next())
{
size_t nText;
UTF8 *attrib = atr_get_LEN(thing, ca, &aowner, &aflags, &nText);
int matches = pcre2_match(re, attrib, nText, 0, 0,
match_data, nullptr);
if (matches >= 0)
{
if (bp != tbuf1) safe_chr(' ', tbuf1, &bp);
ATTR *ap = atr_num(ca);
const UTF8 *pName = T("(WARNING: Bad Attribute Number)");
if (ap)
{
pName = ap->name;
}
safe_str(pName, tbuf1, &bp);
}
free_lbuf(attrib);
}
*bp = '\0';
pcre2_match_data_free(match_data);
pcre2_code_free(re);
olist_pop();
return tbuf1;
}
static void regrep_handler(UTF8 *buff, UTF8 **bufc, dbref executor,
const UTF8 * const fargs[], bool bCaseInsens)
{
dbref it = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(it))
{
safe_match_result(it, buff, bufc);
return;
}
if (!Examinable(executor, it))
{
safe_noperm(buff, bufc);
return;
}
if (!fargs[1] || !*fargs[1])
{
safe_str(S_("#-1 NO SUCH ATTRIBUTE"), buff, bufc);
return;
}
if (!fargs[2] || !*fargs[2])
{
safe_str(S_("#-1 INVALID GREP PATTERN"), buff, bufc);
return;
}
UTF8 *tp = regrep_util(executor, it, fargs[1], fargs[2], bCaseInsens);
safe_str(tp, buff, bufc);
free_lbuf(tp);
}
FUNCTION(fun_regrep)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
regrep_handler(buff, bufc, executor, fargs, false);
}
FUNCTION(fun_regrepi)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
regrep_handler(buff, bufc, executor, fargs, true);
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_alphamax)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *amax = fargs[0];
LBuf buf_max = LBuf_Src("fun_alphamax");
mux_strncpy(buf_max, strip_color(amax), LBUF_SIZE-1);
for (int i = 1; i < nfargs; i++)
{
if (fargs[i] && strcmp(reinterpret_cast<char *>(buf_max.get()), reinterpret_cast<char *>(strip_color(fargs[i]))) < 0)
{
amax = fargs[i];
mux_strncpy(buf_max, strip_color(amax), LBUF_SIZE-1);
}
}
safe_str(amax, buff, bufc);
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_alphamin)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *amin = fargs[0];
LBuf buf_min = LBuf_Src("fun_alphamin");
mux_strncpy(buf_min, strip_color(amin), LBUF_SIZE-1);
for (int i = 1; i < nfargs; i++)
{
if (fargs[i] && strcmp(reinterpret_cast<char *>(buf_min.get()), reinterpret_cast<char *>(strip_color(fargs[i]))) > 0)
{
amin = fargs[i];
mux_strncpy(buf_min, strip_color(amin), LBUF_SIZE-1);
}
}
safe_str(amin, buff, bufc);
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_valid)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Checks to see if a given <something> is valid as a parameter of
// a given type (such as an object name)
//
size_t nValidName;
bool bValid;
if (!*fargs[0] || !*fargs[1])
{
bValid = false;
}
else if (!mux_stricmp(fargs[0], T("attrname")))
{
MakeCanonicalAttributeName(fargs[1], &nValidName, &bValid);
}
else if (!mux_stricmp(fargs[0], T("comalias")))
{
MakeCanonicalComAlias(fargs[1], &nValidName, &bValid);
}
else if (!mux_stricmp(fargs[0], T("doing")))
{
MakeCanonicalDoing(fargs[1], &nValidName, &bValid);
}
else if (!mux_stricmp(fargs[0], T("exitname")))
{
MakeCanonicalExitName(fargs[1], &nValidName, &bValid);
}
else if (!mux_stricmp(fargs[0], T("malias")))
{
MakeCanonicalMailAlias(fargs[1], &nValidName, &bValid);
}
else if (!mux_stricmp(fargs[0], T("maliasdesc")))
{
size_t vw;
MakeCanonicalMailAliasDesc(fargs[1], &nValidName, &bValid, &vw);
}
else if ( !mux_stricmp(fargs[0], T("name"))
|| !mux_stricmp(fargs[0], T("thingname")))
{
MakeCanonicalObjectName(fargs[1], &nValidName, &bValid, mudconf.thing_name_charset);
}
else if (!mux_stricmp(fargs[0], T("roomname")))
{
MakeCanonicalObjectName(fargs[1], &nValidName, &bValid, mudconf.room_name_charset);
}
else if (!mux_stricmp(fargs[0], T("password")))
{
const UTF8 *msg;
bValid = ok_password(fargs[1], &msg);
}
else if (!mux_stricmp(fargs[0], T("playername")))
{
bValid = ValidatePlayerName(fargs[1]);
}
else
{
safe_nothing(buff, bufc);
return;
}
safe_bool(bValid, buff, bufc);
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_hastype)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref it = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(it))
{
safe_match_result(it, buff, bufc);
return;
}
// Match the full type name (help hastype(): ROOM, EXIT, PLAYER,
// THING; anything else is #-1). This historically tested only
// fargs[1][0], so any string starting with R/E/P/T passed as that
// type, and the error arm fell through to safe_bool and emitted
// "#-1 NO SUCH TYPE0" (#1168).
bool bResult;
if (!mux_stricmp(fargs[1], T("ROOM")))
{
bResult = isRoom(it);
}
else if (!mux_stricmp(fargs[1], T("EXIT")))
{
bResult = isExit(it);
}
else if (!mux_stricmp(fargs[1], T("PLAYER")))
{
bResult = isPlayer(it);
}
else if (!mux_stricmp(fargs[1], T("THING")))
{
bResult = isThing(it);
}
else
{
safe_str(S_("#-1 NO SUCH TYPE"), buff, bufc);
return;
}
safe_bool(bResult, buff, bufc);
}
// Borrowed from PennMUSH 1.50
//
FUNCTION(fun_lparent)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref it = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(it))
{
safe_match_result(it, buff, bufc);
return;
}
else if (!Examinable(executor, it))
{
safe_noperm(buff, bufc);
return;
}
ITL pContext;
ItemToList_Init(&pContext, buff, bufc, '#');
if (!ItemToList_AddInteger(&pContext, it))
{
ItemToList_Final(&pContext);
return;
}
dbref par = Parent(it);
int iNestLevel = 1;
while ( Good_obj(par)
&& Examinable(executor, it)
&& iNestLevel < mudconf.parent_nest_lim)
{
if (!ItemToList_AddInteger(&pContext, par))
{
break;
}
it = par;
par = Parent(par);
iNestLevel++;
}
ItemToList_Final(&pContext);
}
/* ---------------------------------------------------------------------------
* fun_regmatch: Return 0 or 1 depending on whether or not a regular
* expression matches a string. If a third argument is specified, dump
* the results of a regexp pattern match into a set of arbitrary r()-registers.
*
* regmatch(string, pattern, list of registers)
* If the number of matches exceeds the registers, those bits are tossed
* out.
* If -1 is specified as a register number, the matching bit is tossed.
* Therefore, if the list is "-1 0 3 5", the regexp $0 is tossed, and
* the regexp $1, $2, and $3 become r(0), r(3), and r(5), respectively.
*/
// `registers` is const since #2144 routed it through list2arr_nd — the
// compiler now holds what the comment there only asserted (#2157, a
// down-payment on #2136's const-fargs contract).
static void real_regmatch(const UTF8 *search, const UTF8 *pattern,
const UTF8 *registers,
int nfargs, UTF8 *buff, UTF8 **bufc, bool cis)
{
if (alarm_clock.alarmed)
{
return;
}
PCRE2_SIZE erroffset;
int errcode;
// Compile the pattern
pcre2_code *re = pcre2_compile_8(
pattern, // pattern string
PCRE2_ZERO_TERMINATED, // pattern is zero-terminated
PCRE2_UTF|(cis ? PCRE2_CASELESS : 0), // options
&errcode, // for error code
&erroffset, // for error offset
nullptr // use default compile context
);
if (!re)
{
// Matching error - get the error message
PCRE2_UCHAR errbuf[256];
pcre2_get_error_message(errcode, errbuf, sizeof(errbuf));
safe_str(S_("#-1 REGEXP ERROR "), buff, bufc);
safe_str(reinterpret_cast<UTF8 *>(errbuf), buff, bufc);
return;
}
// Create match data block for storing results
pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(re, nullptr);
if (!match_data)
{
pcre2_code_free(re);
safe_str(S_("#-1 REGEXP MATCH DATA ERROR"), buff, bufc);
return;
}
// Do the match
int matches = pcre2_match(
re, // compiled pattern
search, // subject string
PCRE2_ZERO_TERMINATED, // length of subject string
0, // start offset in subject
0, // options
match_data, // block for storing the result
nullptr // use default match context
);
safe_bool(matches > 0, buff, bufc);
// If we don't have a third argument, we're done.
if (nfargs != 3 || matches <= 0)
{
pcre2_match_data_free(match_data);
pcre2_code_free(re);
return;
}
// We need to parse the list of registers. Non-destructive (#2136):
// `registers` is the caller's fargs[2], borrowed memory.
const int NSUBEXP = 2 * MAX_GLOBAL_REGS;
UTF8 *qregs[NSUBEXP];
SEP sep;
sep.n = 1;
memcpy(sep.str, " ", 2);
LBuf scRegs = LBuf_Src("real_regmatch.regs");
int nqregs = list2arr_nd(qregs, NSUBEXP, registers, sep, scRegs);
// Get ovector pointer for accessing capture groups
PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
// Process each requested register
for (int i = 0; i < nqregs; i++)
{
if ( !qregs[i]
|| !*qregs[i])
{
continue;
}
int curq;
if (IsSingleCharReg(qregs[i], curq))
{
// Single-char register (0-9, a-z).
//
if (i < matches)
{
LBuf p = LBuf_Src("fun_regmatch");
// #1112: PCRE2 requires capacity on entry, not 0.
//
PCRE2_SIZE outlen = LBUF_SIZE - 1;
int ret = pcre2_substring_copy_bynumber(
match_data, i, p, &outlen);
if (ret >= 0)
{
size_t n = static_cast<size_t>(outlen);
RegAssign(&mudstate.global_regs[curq], n, p);
}
else
{
// #1112: clear, do not no-op. RegAssign early-returns on
// a null ptr (eval.cpp), so passing nullptr left the
// register at its PREVIOUS value — invisible while every
// capture was empty, but now that captures fill, an unset
// group would leak whatever the caller had setq'd there.
// The named-register branch below already uses T("").
//
RegAssign(&mudstate.global_regs[curq], 0, T(""));
}
}
else
{
// #1112: ditto — more registers listed than captured groups.
//
RegAssign(&mudstate.global_regs[curq], 0, T(""));
}
}
else
{
// Named register.
//
size_t nName = strlen(reinterpret_cast<char *>(qregs[i]));
if (IsValidNamedReg(qregs[i], nName))
{
if (i < matches)
{
LBuf p = LBuf_Src("fun_regmatch");
// #1112: PCRE2 requires capacity on entry, not 0.
//
PCRE2_SIZE outlen = LBUF_SIZE - 1;
int ret = pcre2_substring_copy_bynumber(
match_data, i, p, &outlen);
if (ret >= 0)
{
size_t n = static_cast<size_t>(outlen);
NamedRegAssign(mudstate.named_regs, qregs[i], nName, n, p);
}
else
{
NamedRegAssign(mudstate.named_regs, qregs[i], nName, 0, T(""));
}
}
else
{
NamedRegAssign(mudstate.named_regs, qregs[i], nName, 0, T(""));
}
}
}
}
pcre2_match_data_free(match_data);
pcre2_code_free(re);
}
FUNCTION(fun_regmatch)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regmatch(fargs[0], fargs[1], fargs[2], nfargs, buff, bufc, false);
}
FUNCTION(fun_regmatchi)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regmatch(fargs[0], fargs[1], fargs[2], nfargs, buff, bufc, true);
}
/* ---------------------------------------------------------------------------
* regrab(), regraball(). Like grab() and graball(), using a regular expression
* instead of a wildcard pattern. The versions ending in i are case-insensitive.
*/
static void real_regrab(const UTF8 *search, const UTF8 *pattern, const SEP &sep, UTF8 *buff,
UTF8 **bufc, bool cis, bool all)
{
if (alarm_clock.alarmed)
{
return;
}
PCRE2_SIZE erroffset;
int errcode;
// Compile the pattern
pcre2_code *re = pcre2_compile_8(
pattern, // pattern string
PCRE2_ZERO_TERMINATED, // pattern is zero-terminated
PCRE2_UTF|(cis ? PCRE2_CASELESS : 0), // options
&errcode, // for error code
&erroffset, // for error offset
nullptr // use default compile context
);
if (!re)
{
// Matching error - get the error message
PCRE2_UCHAR errbuf[256];
pcre2_get_error_message(errcode, errbuf, sizeof(errbuf));
safe_str(S_("#-1 REGEXP ERROR "), buff, bufc);
safe_str(reinterpret_cast<UTF8 *>(errbuf), buff, bufc);
return;
}
// Create match data block for storing results
pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(re, nullptr);
if (!match_data)
{
pcre2_code_free(re);
safe_str(S_("#-1 REGEXP MATCH DATA ERROR"), buff, bufc);
return;
}
// JIT compile if we're going to use the pattern multiple times
if (all)
{
pcre2_jit_compile(re, PCRE2_JIT_COMPLETE);
}
bool first = true;
LBuf scList = LBuf_Src("real_regrab.list");
UTF8 *s = trim_space_sep(list_copy_for_split(scList, search), sep);
do
{
UTF8 *r = split_token(&s, sep);
if (!alarm_clock.alarmed)
{
int rc = pcre2_match(
re, // compiled pattern
r, // subject string
PCRE2_ZERO_TERMINATED, // length of subject string
0, // start offset in subject
0, // options
match_data, // block for storing the result
nullptr // use default match context
);
if (rc >= 0)
{
if (first)
{
first = false;
}
else
{
print_sep(sep, buff, bufc);
}
safe_str(r, buff, bufc);
if (!all)
{
break;
}
}
}
} while (s);
pcre2_match_data_free(match_data);
pcre2_code_free(re);
}
FUNCTION(fun_regrab)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
real_regrab(fargs[0], fargs[1], sep, buff, bufc, false, false);
}
FUNCTION(fun_regrabi)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
real_regrab(fargs[0], fargs[1], sep, buff, bufc, true, false);
}
FUNCTION(fun_regraball)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
real_regrab(fargs[0], fargs[1], sep, buff, bufc, false, true);
}
FUNCTION(fun_regraballi)
{
SEP sep;
if (!OPTIONAL_DELIM(3, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
real_regrab(fargs[0], fargs[1], sep, buff, bufc, true, true);
}
/* ---------------------------------------------------------------------------
* regedit(), regediti(), regeditall(), regeditalli(). Regex-based
* find-and-replace on strings, borrowing Penn's concept. Supports $0-$99
* for numbered capture groups and $<name> for named capture groups in the
* replacement string.
*/
// regedit_substitute: Expand $N and $<name> references in a replacement
// string using match data from a PCRE2 match.
//
static void regedit_substitute(const UTF8 *replacement,
pcre2_match_data *match_data, UTF8 *buff, UTF8 **bufc)
{
const UTF8 *p = replacement;
while (*p)
{
if ('$' == *p)
{
p++;
if ('<' == *p)
{
// Named capture group: $<name>
//
p++;
const UTF8 *namestart = p;
while ( *p
&& '>' != *p)
{
p++;
}
if ('>' == *p)
{
size_t namelen = p - namestart;
p++;
// Build null-terminated name.
//
UTF8 namebuf[128];
if (namelen < sizeof(namebuf))
{
memcpy(namebuf, namestart, namelen);
namebuf[namelen] = '\0';
LBuf groupbuf = LBuf_Src("regsub_named");
PCRE2_SIZE outlen = LBUF_SIZE - 1;
int ret = pcre2_substring_copy_byname(
match_data,
namebuf,
groupbuf,
&outlen
);
if (ret >= 0)
{
safe_copy_buf(groupbuf, outlen, buff, bufc);
}
}
}
else
{
// Unterminated $<...>, output literally.
//
safe_chr('$', buff, bufc);
safe_chr('<', buff, bufc);
safe_str(namestart, buff, bufc);
}
}
else if (mux_isdigit(*p))
{
// Numbered capture group: $0 through $99
//
int groupnum = *p - '0';
p++;
if (mux_isdigit(*p))
{
groupnum = groupnum * 10 + (*p - '0');
p++;
}
LBuf groupbuf = LBuf_Src("regsub_numbered");
PCRE2_SIZE outlen = LBUF_SIZE - 1;
int ret = pcre2_substring_copy_bynumber(
match_data,
groupnum,
groupbuf,
&outlen
);
if (ret >= 0)
{
safe_copy_buf(groupbuf, outlen, buff, bufc);
}
}
else
{
// Lone $ not followed by digit or <, output literally.
//
safe_chr('$', buff, bufc);
}
}
else
{
safe_chr(*p, buff, bufc);
p++;
}
}
}
static void real_regedit(const UTF8 * const fargs[], int nfargs, UTF8 *buff,
UTF8 **bufc, bool cis, bool all)
{
if (alarm_clock.alarmed)
{
return;
}
// Use two lbufs as ping-pong buffers for multi-pair processing.
//
UTF8 *inbuf = alloc_lbuf("regedit.in");
UTF8 *outbuf = alloc_lbuf("regedit.out");
mux_strncpy(inbuf, fargs[0], LBUF_SIZE - 1);
for (int i = 1; i + 1 < nfargs; i += 2)
{
if (alarm_clock.alarmed)
{
break;
}
PCRE2_SIZE erroffset;
int errcode;
pcre2_code *re = pcre2_compile_8(
fargs[i],
PCRE2_ZERO_TERMINATED,
PCRE2_UTF | (cis ? PCRE2_CASELESS : 0),
&errcode,
&erroffset,
nullptr
);
if (!re)
{
PCRE2_UCHAR errbuf[256];
pcre2_get_error_message(errcode, errbuf, sizeof(errbuf));
free_lbuf(inbuf);
free_lbuf(outbuf);
safe_str(S_("#-1 REGEXP ERROR "), buff, bufc);
safe_str(reinterpret_cast<UTF8 *>(errbuf), buff, bufc);
return;
}
pcre2_match_data *match_data =
pcre2_match_data_create_from_pattern(re, nullptr);
if (!match_data)
{
pcre2_code_free(re);
free_lbuf(inbuf);
free_lbuf(outbuf);
safe_str(S_("#-1 REGEXP MATCH DATA ERROR"), buff, bufc);
return;
}
if (all)
{
pcre2_jit_compile(re, PCRE2_JIT_COMPLETE);
}
UTF8 *outp = outbuf;
PCRE2_SIZE pos = 0;
PCRE2_SIZE inlen = strlen(reinterpret_cast<char *>(inbuf));
bool matched = false;
while (pos <= inlen)
{
if (alarm_clock.alarmed)
{
break;
}
int rc = pcre2_match(
re,
inbuf,
inlen,
pos,
0,
match_data,
nullptr
);
if (rc < 0)
{
// No more matches. Copy rest of input.
//
safe_copy_buf(inbuf + pos, inlen - pos, outbuf, &outp);
break;
}
matched = true;
PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
// Copy text before this match.
//
if (ovector[0] > pos)
{
safe_copy_buf(inbuf + pos, ovector[0] - pos, outbuf, &outp);
}
// Expand replacement string with capture group substitution.
//
regedit_substitute(fargs[i + 1], match_data, outbuf, &outp);
if (!all)
{
// Copy remaining text after the first match.
//
safe_copy_buf(inbuf + ovector[1], inlen - ovector[1],
outbuf, &outp);
break;
}
// Advance past this match. Handle zero-length matches by
// advancing one byte to avoid infinite loops.
//
if (ovector[1] == pos)
{
if (pos < inlen)
{
safe_chr(inbuf[pos], outbuf, &outp);
}
pos++;
}
else
{
pos = ovector[1];
}
}
*outp = '\0';
pcre2_match_data_free(match_data);
pcre2_code_free(re);
// Swap buffers for next pair.
//
UTF8 *tmp = inbuf;
inbuf = outbuf;
outbuf = tmp;
}
safe_str(inbuf, buff, bufc);
free_lbuf(inbuf);
free_lbuf(outbuf);
}
FUNCTION(fun_regedit)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regedit(fargs, nfargs, buff, bufc, false, false);
}
FUNCTION(fun_regediti)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regedit(fargs, nfargs, buff, bufc, true, false);
}
FUNCTION(fun_regeditall)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regedit(fargs, nfargs, buff, bufc, false, true);
}
FUNCTION(fun_regeditalli)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
real_regedit(fargs, nfargs, buff, bufc, true, true);
}
/* ---------------------------------------------------------------------------
* fun_translate: Takes a string and a second argument. If the second argument
* is 0 or s, control characters are converted to spaces. If it's 1 or p,
* they're converted to percent substitutions.
*/
FUNCTION(fun_translate)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int ch = fargs[1][0];
bool type = (ch == 'p' || ch == '1');
safe_str(translate_string(fargs[0], type), buff, bufc);
}
// -------------------------------------------------------------------------
// fun_lrooms: Takes a dbref (room), an int (N), and an optional bool (B).
//
// MUX Syntax: lrooms(<room> [,<N>[,<B>]])
//
// Returns a list of rooms <N>-levels deep from <room>. If <B> == 1, it will
// return all room dbrefs between 0 and <N> levels, while <B> == 0 will
// return only the room dbrefs on the Nth level. The default is to show all
// rooms dbrefs between 0 and <N> levels.
//
// Written by Marlek. Idea from RhostMUSH.
//
static void room_list
(
dbref player,
dbref enactor,
dbref room,
int maxlevels,
bool showall
)
{
// BFS level-by-level traversal. 'current' holds rooms at depth d,
// 'next' collects rooms at depth d+1.
//
std::vector<dbref> current;
std::vector<dbref> next;
current.push_back(room);
for (int d = 0; d < maxlevels; d++)
{
for (size_t ci = 0; ci < current.size(); ci++)
{
dbref r = current[ci];
int lev;
dbref parent;
ITER_PARENTS(r, parent, lev)
{
if (!Has_exits(parent))
{
continue;
}
int key = 0;
if (Examinable(player, parent))
{
key |= VE_LOC_XAM;
}
if (Dark(parent))
{
key |= VE_LOC_DARK;
}
if (Dark(r))
{
key |= VE_BASE_DARK;
}
dbref thing;
DOLIST(thing, Exits(parent))
{
dbref loc = Location(thing);
// #1108: Unlinked exits leave Location == NOTHING (-1).
// IsSet/Set no-op on out-of-range, but Examinable → Flags/
// Owner → db[-1] is an OOB read. Only walk rooms.
//
if ( !Good_obj(loc)
|| !isRoom(loc))
{
continue;
}
if ( exit_visible(thing, player, key)
&& !mudstate.bfTraverse.IsSet(loc))
{
mudstate.bfTraverse.Set(loc);
if ( ( showall
|| d + 1 == maxlevels)
&& ( Examinable(player, loc)
|| Location(player) == loc
|| loc == enactor))
{
mudstate.bfReport.Set(loc);
}
next.push_back(loc);
}
}
}
}
current.swap(next);
next.clear();
}
}
FUNCTION(fun_lrooms)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref room = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(room))
{
safe_match_result(room, buff, bufc);
return;
}
else if (!isRoom(room))
{
safe_str(S_("#-1 FIRST ARGUMENT MUST BE A ROOM"), buff, bufc);
return;
}
int N = 1;
if (nfargs >= 2)
{
N = mux_atoi64(fargs[1]);
if (N < 0)
{
safe_str(S_("#-1 SECOND ARGUMENT MUST BE A POSITIVE NUMBER"),
buff, bufc);
return;
}
else if (N > 50)
{
// Maybe this can be turned into a config parameter to prevent
// misuse by putting in really large values.
//
safe_str(S_("#-1 SECOND ARGUMENT IS TOO LARGE"), buff, bufc);
return;
}
}
bool B = true;
if (nfargs == 3)
{
B = xlate(fargs[2]);
}
mudstate.bfReport.Resize(mudstate.db_top-1);
mudstate.bfTraverse.Resize(mudstate.db_top-1);
mudstate.bfReport.ClearAll();
mudstate.bfTraverse.ClearAll();
mudstate.bfTraverse.Set(room);
room_list(executor, enactor, room, N, B);
mudstate.bfReport.Clear(room);
ITL pContext;
ItemToList_Init(&pContext, buff, bufc, '#');
dbref i;
DO_WHOLE_DB(i)
{
if ( mudstate.bfReport.IsSet(i)
&& !ItemToList_AddInteger(&pContext, i))
{
break;
}
}
ItemToList_Final(&pContext);
}
// ---------------------------------------------------------------------------
// fun_route: next-hop routing over NAVIGABLE rooms.
//
// route(<source>, <destination>[, <options>])
//
// Options (space-separated): distance, path, rebuild.
// See docs/design-routing.md for the full specification.
// ---------------------------------------------------------------------------
#include "routing.h"
FUNCTION(fun_route)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref source = match_thing_quiet(executor, fargs[0]);
if (!Good_obj(source))
{
safe_match_result(source, buff, bufc);
return;
}
dbref destination = match_thing_quiet(executor, fargs[1]);
if (!Good_obj(destination))
{
safe_match_result(destination, buff, bufc);
return;
}
// Parse options (third argument, space-separated).
//
int options = 0;
if (nfargs >= 3 && fargs[2] && *fargs[2])
{
LBuf scOpts = LBuf_Src("fun_route.opts");
UTF8 *opts = trim_space_sep(list_copy_for_split(scOpts, fargs[2]), sepSpace);
UTF8 *token;
while (opts && *opts)
{
token = split_token(&opts, sepSpace);
if (mux_stricmp(token, T("distance")) == 0)
{
options |= ROUTE_OPT_DISTANCE;
}
else if (mux_stricmp(token, T("path")) == 0)
{
options |= ROUTE_OPT_PATH;
}
else if (mux_stricmp(token, T("rebuild")) == 0)
{
options |= ROUTE_OPT_REBUILD;
}
else if (mux_stricmp(token, T("locked")) == 0)
{
options |= ROUTE_OPT_LOCKED;
}
else
{
safe_str(S_("#-2"), buff, bufc);
return;
}
}
}
route_query(executor, source, destination, options, buff, bufc);
}