tinymux/mux/modules/engine/boolexp.cpp
Stephen Dennis 8c466171c5 nls: mark object/flags/boolexp create-and-flag notify prose with M_()
Phase 3 coverage slice. Player and staff notifies that still used T()
(cast-only) now use M_() so they extract into the catalogue:

  object  name taken/silly, deposit refund, @chown summary, parent/zone/
          home/dropto clears, floating room
  flags   Flags: header, type/flag parse errors, @set decompile, flag
          name removed
  boolexp match “don’t see / which”
  wiz     password changed by %s
  player  @protect all listing line

Left alone: log messages, HTML, softcode/machine tokens, concatenated
fragments (set @chown owner line, rob give pieces), and punctuation
separators. pot 704 -> 722; xx filled; ko msgmerge only.
2026-07-28 13:06:50 +00:00

853 lines
22 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \file boolexp.cpp
* \brief Boolean Expressions for \@locks
*
* The functions here evaluate and organize boolean expressions used in
* locks. Lock evaluation contains one of server's the three parsers. The
* other two parsers are for functions (see eval.cpp) and commands (see
* command.cpp).
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
static bool parsing_internal = false;
// Bound parser recursion. parse_boolexp_E/T/F/L are mutually recursive on
// attacker-controlled @lock input, with no depth limit of their own — the
// existing mudconf.lock_nest_lim guards only the *evaluator's* indirect-lock
// recursion. A deeply nested lock such as "!!!!...#1" or "((((...#1" therefore
// overflows the C stack and crashes the server. Real locks nest a handful of
// levels; this cap is far beyond any legitimate use yet well within the stack
// (parse_boolexp_F's scratch buffer is heap-allocated, below, so each frame is
// small). Tripping it yields TRUE_BOOLEXP, the same sentinel the parser already
// returns for any malformed lock.
//
static constexpr int LOCK_PARSE_MAX_DEPTH = 1024;
static thread_local int s_parse_depth = 0;
namespace
{
class ParseDepthGuard
{
public:
ParseDepthGuard() { ++s_parse_depth; }
~ParseDepthGuard() { --s_parse_depth; }
};
}
/* ---------------------------------------------------------------------------
* check_attr: indicate if attribute ATTR on player passes key when checked by
* the object lockobj
*/
static bool check_attr(dbref player, dbref lockobj, ATTR *attr, UTF8 *key)
{
dbref aowner;
int aflags;
bool bCheck = false;
LBuf buff = LBuf_Adopt(atr_pget(player, attr->number, &aowner, &aflags));
if (attr->number == A_LENTER)
{
// We can see enterlocks... else we'd break zones.
//
bCheck = true;
}
else if (See_attr(lockobj, player, attr))
{
bCheck = true;
}
else if (attr->number == A_NAME)
{
bCheck = true;
}
if ( bCheck
&& !wild_match(key, buff))
{
bCheck = false;
}
return bCheck;
}
bool eval_boolexp(dbref player, dbref thing, dbref from, BOOLEXP *b)
{
if (b == TRUE_BOOLEXP)
{
return true;
}
dbref aowner, obj, source;
int aflags;
UTF8 *buff, *bp;
UTF8 *key;
ATTR *a;
bool bCheck, c;
switch (b->type)
{
case BOOLEXP_AND:
return eval_boolexp(player, thing, from, b->sub1)
&& eval_boolexp(player, thing, from, b->sub2);
case BOOLEXP_OR:
return eval_boolexp(player, thing, from, b->sub1)
|| eval_boolexp(player, thing, from, b->sub2);
case BOOLEXP_NOT:
return !eval_boolexp(player, thing, from, b->sub1);
case BOOLEXP_INDIR:
// BOOLEXP_INDIR (i.e. @) is a unary operation which is replaced at
// evaluation time by the lock of the object whose number is the
// argument of the operation.
//
mudstate.lock_nest_lev++;
if (mudstate.lock_nest_lev >= mudconf.lock_nest_lim)
{
if (mudstate.bStandAlone)
{
Log.WriteString(T("Lock exceeded recursion limit." ENDLINE));
}
else
{
STARTLOG(LOG_BUGS, "BUG", "LOCK");
log_name_and_loc(player);
log_text(T(": Lock exceeded recursion limit."));
ENDLOG;
notify(player, M_("Sorry, broken lock!"));
}
mudstate.lock_nest_lev--;
return false;
}
if ( b->sub1->type != BOOLEXP_CONST
|| b->sub1->thing < 0)
{
if (mudstate.bStandAlone)
{
Log.WriteString(T("Broken lock." ENDLINE));
}
else
{
STARTLOG(LOG_BUGS, T("BUG"), T("LOCK"));
log_name_and_loc(player);
buff = alloc_mbuf("eval_boolexp.LOG.indir");
mux_sprintf(buff, MBUF_SIZE, T(": Lock had bad indirection (%c, type %d)"),
INDIR_TOKEN, b->sub1->type);
log_text(buff);
free_mbuf(buff);
ENDLOG;
notify(player, M_("Sorry, broken lock!"));
}
mudstate.lock_nest_lev--;
return false;
}
{
LBuf lkey = LBuf_Adopt(atr_get("boolexp.130", b->sub1->thing, b->thing, &aowner, &aflags));
c = eval_boolexp_atr(player, b->sub1->thing, from, lkey);
}
mudstate.lock_nest_lev--;
return c;
case BOOLEXP_CONST:
return b->thing == player
|| member(b->thing, Contents(player));
case BOOLEXP_ATR:
a = atr_num(b->thing);
if (!a)
{
// No such attribute.
//
return false;
}
// First check the object itself, then its contents.
//
if (check_attr(player, from, a, reinterpret_cast<UTF8*>(b->sub1)))
{
return true;
}
DOLIST(obj, Contents(player))
{
if (check_attr(obj, from, a, reinterpret_cast<UTF8*>(b->sub1)))
{
return true;
}
}
return false;
case BOOLEXP_EVAL:
a = atr_num(b->thing);
if ( !a
|| alarm_clock.alarmed)
{
// No such attribute.
//
return false;
}
source = from;
{
LBuf lbuff = LBuf_Adopt(atr_pget(from, a->number, &aowner, &aflags));
if (!lbuff.get() || !*lbuff.get())
{
lbuff = LBuf_Adopt(atr_pget(thing, a->number, &aowner, &aflags));
source = thing;
}
bCheck = false;
if ( a->number == A_NAME
|| a->number == A_LENTER
|| bCanReadAttr(source, source, a, false))
{
bCheck = true;
}
if (bCheck)
{
if ((aflags & AF_NOEVAL) || NoEval(source))
{
bCheck = !string_compare(lbuff, reinterpret_cast<UTF8*>(b->sub1));
}
else
{
reg_ref** preserve = PushRegisters(MAX_GLOBAL_REGS);
save_global_regs(preserve);
LBuf buff2 = LBuf_Src("eval_boolexp");
bp = buff2.get();
mux_exec(lbuff, LBUF_SIZE-1, buff2, &bp, source, player, player,
AttrTrace(aflags, EV_FIGNORE|EV_EVAL|EV_FCHECK|EV_TOP),
nullptr, 0);
*bp = '\0';
restore_global_regs(preserve);
PopRegisters(preserve, MAX_GLOBAL_REGS);
bCheck = !string_compare(buff2, reinterpret_cast<UTF8*>(b->sub1));
}
}
}
return bCheck;
case BOOLEXP_IS:
// If an object check, do that.
//
if (b->sub1->type == BOOLEXP_CONST)
{
return (b->sub1->thing == player);
}
// Nope, do an attribute check
//
a = atr_num(b->sub1->thing);
if (!a)
{
return false;
}
return check_attr(player, from, a, reinterpret_cast<UTF8*>((b->sub1)->sub1));
case BOOLEXP_CARRY:
// If an object check, do that
//
if (b->sub1->type == BOOLEXP_CONST)
{
return member(b->sub1->thing, Contents(player));
}
// Nope, do an attribute check
//
a = atr_num(b->sub1->thing);
if (!a)
{
return false;
}
DOLIST(obj, Contents(player))
{
if (check_attr(obj, from, a, reinterpret_cast<UTF8*>((b->sub1)->sub1)))
{
return true;
}
}
return false;
case BOOLEXP_OWNER:
return (Owner(b->sub1->thing) == Owner(player));
default:
// Bad type
//
mux_assert(0);
return false;
}
}
bool eval_boolexp_atr(dbref player, dbref thing, dbref from, UTF8 *key)
{
bool ret_value;
BOOLEXP *b = parse_boolexp(player, key, true);
if (b == nullptr)
{
ret_value = true;
}
else
{
ret_value = eval_boolexp(player, thing, from, b);
free_boolexp(b);
}
return ret_value;
}
// If the parser returns TRUE_BOOLEXP, you lose
// TRUE_BOOLEXP cannot be typed in by the user; use @unlock instead
//
static const char *parsebuf;
thread_local char parsestore[LBUF_SIZE];
static dbref parse_player;
static void skip_whitespace(void)
{
while (mux_isspace(*parsebuf))
{
parsebuf++;
}
}
// Defined below.
//
static BOOLEXP *parse_boolexp_E(void);
static BOOLEXP *test_atr(UTF8 *s)
{
int anum;
boolexp_type locktype;
LBuf buff = LBuf_Src("test_atr");
mux_strncpy(buff, s, LBUF_SIZE-1);
for (s = buff.get(); *s && (*s != ':') && (*s != '/'); s++)
{
}
if (!*s)
{
return TRUE_BOOLEXP;
}
if (*s == '/')
{
locktype = BOOLEXP_EVAL;
}
else
{
locktype = BOOLEXP_ATR;
}
*s++ = '\0';
// See if left side is valid attribute. Access to attr is checked on eval
// Also allow numeric references to attributes. It can't hurt us, and lets
// us import stuff that stores attr locks by number instead of by name.
//
ATTR *attrib = atr_str(buff);
if (!attrib)
{
UTF8 *s1;
// Only #1 can lock on numbers
//
if (!God(parse_player))
{
return TRUE_BOOLEXP;
}
for (s1 = buff; mux_isdigit(*s1); s1++)
{
}
if (*s1)
{
return TRUE_BOOLEXP;
}
anum = mux_atoi64(buff);
if (anum <= 0)
{
return TRUE_BOOLEXP;
}
}
else
{
anum = attrib->number;
}
// made it now make the parse tree node
//
auto b = alloc_bool("test_str");
b->type = locktype;
b->thing = static_cast<dbref>(anum);
b->sub1 = reinterpret_cast<BOOLEXP*>(StringClone(s));
return b;
}
// L -> (E); L -> object identifier
//
static BOOLEXP *parse_boolexp_L(void)
{
BOOLEXP *b;
UTF8 *p;
MSTATE mstate;
skip_whitespace();
switch (*parsebuf)
{
case '(':
parsebuf++;
b = parse_boolexp_E();
skip_whitespace();
if ( b == TRUE_BOOLEXP
|| *parsebuf++ != ')')
{
free_boolexp(b);
return TRUE_BOOLEXP;
}
break;
default:
// Must have hit an object ref. Load the name into our buffer.
//
LBuf buf = LBuf_Src("parse_boolexp_L");
p = buf.get();
while ( *parsebuf
&& *parsebuf != AND_TOKEN
&& *parsebuf != OR_TOKEN
&& *parsebuf != ')'
&& p < buf.get() + LBUF_SIZE)
{
*p++ = *parsebuf++;
}
// Strip trailing whitespace.
//
*p-- = '\0';
while (mux_isspace(*p))
{
*p-- = '\0';
}
// Check for an attribute.
//
if ((b = test_atr(buf)) != nullptr)
{
return (b);
}
b = alloc_bool("parse_boolexp_L");
b->type = BOOLEXP_CONST;
// do the match.
//
if (!mudstate.bStandAlone)
{
// If we are parsing a boolexp that was a stored lock then we
// know that object refs are all dbrefs, so we skip the
// expensive match code.
//
if (parsing_internal)
{
if (buf[0] != '#')
{
free_bool(b);
return TRUE_BOOLEXP;
}
b->thing = mux_atoi64(&buf[1]);
if (!Good_dbref(b->thing))
{
free_bool(b);
return TRUE_BOOLEXP;
}
}
else
{
save_match_state(&mstate);
init_match(parse_player, buf, TYPE_THING);
match_everything(MAT_EXIT_PARENTS);
b->thing = match_result();
restore_match_state(&mstate);
}
if (b->thing == NOTHING)
{
notify(parse_player, tprintf(M_("I dont see %s here."), buf.get()));
free_bool(b);
return TRUE_BOOLEXP;
}
if (b->thing == AMBIGUOUS)
{
notify(parse_player, tprintf(M_("I dont know which %s you mean!"),
buf.get()));
free_bool(b);
return TRUE_BOOLEXP;
}
}
else
{
// Had better be #<num> or we're hosed.
//
if (buf[0] != '#')
{
free_bool(b);
return TRUE_BOOLEXP;
}
b->thing = mux_atoi64(&buf[1]);
if (b->thing < 0)
{
free_bool(b);
return TRUE_BOOLEXP;
}
}
}
return b;
}
// F -> !F; F -> @L; F -> =L; F -> +L; F -> $L
// The argument L must be type BOOLEXP_CONST
//
static BOOLEXP *parse_boolexp_F(void)
{
// Every parser recursion cycle (NOT's F->F, AND's T->F, OR's E->T->F,
// parens' L->E->T->F) passes through here, so bounding depth at this one
// point bounds all of them.
//
ParseDepthGuard depth_guard;
if (s_parse_depth > LOCK_PARSE_MAX_DEPTH)
{
return TRUE_BOOLEXP;
}
BOOLEXP *b2;
skip_whitespace();
switch (*parsebuf)
{
case NOT_TOKEN:
parsebuf++;
b2 = alloc_bool("parse_boolexp_F.not");
b2->type = BOOLEXP_NOT;
if ((b2->sub1 = parse_boolexp_F()) == TRUE_BOOLEXP)
{
free_boolexp(b2);
return (TRUE_BOOLEXP);
}
else
{
return (b2);
}
// NOTREACHED
//
case INDIR_TOKEN:
parsebuf++;
b2 = alloc_bool("parse_boolexp_F.indir");
b2->type = BOOLEXP_INDIR;
b2->thing = A_LOCK;
// Scan ahead for '/' (lock name separator) before any operator.
// We need to find it before parse_boolexp_L() consumes it.
//
{
const char *slash = nullptr;
int depth = 0;
for (const char *scan = parsebuf; *scan; scan++)
{
if (*scan == '(')
{
depth++;
}
else if (*scan == ')')
{
if (depth > 0)
{
depth--;
}
else
{
break;
}
}
else if ( depth == 0
&& ( *scan == AND_TOKEN
|| *scan == OR_TOKEN))
{
break;
}
else if ( depth == 0
&& *scan == '/')
{
slash = scan;
break;
}
}
if (slash)
{
// Copy the object-ref portion into a local buffer and
// parse it separately, then extract and look up the lock
// name that follows the '/'.
//
size_t objlen = static_cast<size_t>(slash - parsebuf);
// Heap-allocated (not a 32 KB stack array) so that deep parser
// recursion does not balloon the stack frame.
LBuf objbuf_lbuf = LBuf_Src("parse_boolexp_F.objbuf");
char *objbuf = reinterpret_cast<char *>(objbuf_lbuf.get());
if (objlen >= LBUF_SIZE)
{
objlen = LBUF_SIZE - 1;
}
memcpy(objbuf, parsebuf, objlen);
objbuf[objlen] = '\0';
const char *saved = parsebuf;
parsebuf = objbuf;
b2->sub1 = parse_boolexp_L();
parsebuf = saved;
// Advance past the object ref and the '/'.
//
const char *lockname_start = slash + 1;
const char *lockname_end = lockname_start;
while ( *lockname_end
&& *lockname_end != AND_TOKEN
&& *lockname_end != OR_TOKEN
&& *lockname_end != ')')
{
lockname_end++;
}
// Strip trailing whitespace from the lock name.
//
const char *lockname_trim = lockname_end;
while ( lockname_trim > lockname_start
&& mux_isspace(*(lockname_trim - 1)))
{
lockname_trim--;
}
char lockname[SBUF_SIZE];
size_t lnlen = static_cast<size_t>(lockname_trim - lockname_start);
if (lnlen >= sizeof(lockname))
{
lnlen = sizeof(lockname) - 1;
}
memcpy(lockname, lockname_start, lnlen);
lockname[lnlen] = '\0';
parsebuf = lockname_end;
// Look up the lock name.
//
int lock_attr;
if (!search_nametab(parse_player, lock_sw,
reinterpret_cast<const UTF8 *>(lockname), &lock_attr))
{
if (!mudstate.bStandAlone)
{
notify(parse_player, M_("Unknown lock type."));
}
free_boolexp(b2);
return TRUE_BOOLEXP;
}
b2->thing = static_cast<dbref>(lock_attr);
}
else
{
b2->sub1 = parse_boolexp_L();
}
}
if ((b2->sub1) == TRUE_BOOLEXP)
{
free_boolexp(b2);
return (TRUE_BOOLEXP);
}
else if ((b2->sub1->type) != BOOLEXP_CONST)
{
free_boolexp(b2);
return (TRUE_BOOLEXP);
}
else
{
return (b2);
}
// NOTREACHED
//
case IS_TOKEN:
parsebuf++;
b2 = alloc_bool("parse_boolexp_F.is");
b2->type = BOOLEXP_IS;
b2->sub1 = parse_boolexp_L();
if (b2->sub1 == TRUE_BOOLEXP)
{
free_boolexp(b2);
return (TRUE_BOOLEXP);
}
else if ( b2->sub1->type != BOOLEXP_CONST
&& b2->sub1->type != BOOLEXP_ATR)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
else
{
return (b2);
}
// NOTREACHED
//
case CARRY_TOKEN:
parsebuf++;
b2 = alloc_bool("parse_boolexp_F.carry");
b2->type = BOOLEXP_CARRY;
b2->sub1 = parse_boolexp_L();
if (b2->sub1 == TRUE_BOOLEXP)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
else if ( b2->sub1->type != BOOLEXP_CONST
&& b2->sub1->type != BOOLEXP_ATR)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
else
{
return b2;
}
// NOTREACHED
//
case OWNER_TOKEN:
parsebuf++;
b2 = alloc_bool("parse_boolexp_F.owner");
b2->type = BOOLEXP_OWNER;
b2->sub1 = parse_boolexp_L();
if (b2->sub1 == TRUE_BOOLEXP)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
else if (b2->sub1->type != BOOLEXP_CONST)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
else
{
return b2;
}
// NOTREACHED
//
default:
return parse_boolexp_L();
}
}
// T -> F; T -> F & T
//
static BOOLEXP *parse_boolexp_T(void)
{
BOOLEXP *b;
if ((b = parse_boolexp_F()) != TRUE_BOOLEXP)
{
skip_whitespace();
if (*parsebuf == AND_TOKEN)
{
parsebuf++;
const auto b2 = alloc_bool("parse_boolexp_T");
b2->type = BOOLEXP_AND;
b2->sub1 = b;
if ((b2->sub2 = parse_boolexp_T()) == TRUE_BOOLEXP)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
b = b2;
}
}
return b;
}
// E -> T; E -> T | E
//
static BOOLEXP *parse_boolexp_E(void)
{
BOOLEXP *b, *b2;
if ((b = parse_boolexp_T()) != TRUE_BOOLEXP)
{
skip_whitespace();
if (*parsebuf == OR_TOKEN)
{
parsebuf++;
b2 = alloc_bool("parse_boolexp_E");
b2->type = BOOLEXP_OR;
b2->sub1 = b;
if ((b2->sub2 = parse_boolexp_E()) == TRUE_BOOLEXP)
{
free_boolexp(b2);
return TRUE_BOOLEXP;
}
b = b2;
}
}
return b;
}
BOOLEXP *parse_boolexp(dbref player, const UTF8 *buf, bool internal)
{
if ( nullptr == buf
|| '\0' == buf[0])
{
return TRUE_BOOLEXP;
}
size_t n = strlen(reinterpret_cast<const char *>(buf));
if (n > sizeof(parsestore)-1)
{
// Truncate oversize keys. Always write an explicit NUL — memcpy of
// n+1 from a longer source does not leave parsestore terminated and
// the recursive-descent scanner would walk off the buffer.
//
n = sizeof(parsestore)-1;
}
memcpy(parsestore, buf, n);
parsestore[n] = '\0';
parsebuf = parsestore;
parse_player = player;
s_parse_depth = 0;
if (!mudstate.bStandAlone)
{
parsing_internal = internal;
}
return parse_boolexp_E();
}