tinymux/mux/modules/engine/db_rw.cpp
Stephen Dennis 9a4de32dcb fix(queue,db): scheduler enqueue success and flatfile write failures (#1871 #1869)
DeferTask/DeferImmediateTask return bool (nothrow OOM); wait_que and
sql_que free the BQUE and refund quota/waitcost when enqueue fails.
db_write_object reports stream status via ferror; db_write returns -1
on I/O failure after header/object/end/flush checks, and dump/dbconvert
callers refuse to publish a truncated flatfile.
2026-07-31 09:09:18 -06:00

1349 lines
42 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 db_rw.cpp
* \brief Flatfile implementation.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
static int g_version;
static int g_format;
static int g_flags;
// Migrate V4 PUA 24-bit color encoding to V5.
//
// Old format: BMP base (EF 98-9F xx) + per-channel deltas F3 B0 (80-97) xx
// New format: BMP base + F3 (B0-B3) xx xx (2-code-point per layer)
//
// V4 only emitted deltas for channels that differed from the indexed palette
// base. Omitted channels must be filled from the preceding BMP palette entry,
// not zero.
//
// Returns true if the attribute was modified and the output buffer
// contains the complete migrated value. Returns false if no old SMP
// codes were found OR if the migrated output would exceed the buffer
// (in which case the attribute is left untouched — the indexed base
// color survives; only the 24-bit refinement is lost).
//
static bool MigrateColorV4toV5(const UTF8 *pOld, UTF8 *pNew, size_t nBufSize, size_t *pnNew)
{
const UTF8 *p = pOld;
UTF8 *q = pNew;
const UTF8 *qEnd = pNew + nBufSize - 9; // room for 8 bytes + NUL
bool bChanged = false;
// Track the last FG and BG palette index seen, so we can seed
// omitted channels from the palette base.
//
int lastFGIdx = -1;
int lastBGIdx = -1;
while ('\0' != *p)
{
if (q >= qEnd)
{
// Migration would exceed buffer. Abort entirely — leave
// the attribute in its original V4 form rather than store
// a truncated value.
//
return false;
}
// Track BMP PUA FG/BG indexed codes as they pass through.
// FG: EF (98-9B) (80-BF) → index = ((byte1 - 0x98) << 6) | (byte2 - 0x80)
// BG: EF (9C-9F) (80-BF) → index = ((byte1 - 0x9C) << 6) | (byte2 - 0x80)
//
if ( 0xEF == p[0]
&& p[1] >= 0x98 && p[1] <= 0x9F
&& p[2] >= 0x80 && p[2] <= 0xBF)
{
if (p[1] <= 0x9B)
{
lastFGIdx = ((int)(p[1] - 0x98) << 6) | (int)(p[2] - 0x80);
}
else
{
lastBGIdx = ((int)(p[1] - 0x9C) << 6) | (int)(p[2] - 0x80);
}
*q++ = *p++;
*q++ = *p++;
*q++ = *p++;
continue;
}
// Check for old SMP PUA: F3 B0 (80-97) xx
//
if ( 0xF3 == p[0]
&& 0xB0 == p[1]
&& p[2] >= 0x80 && p[2] <= 0x97
&& p[3] >= 0x80 && p[3] <= 0xBF)
{
// Decode old format: 6 channels × 256 values
//
unsigned int offset = ((unsigned int)(p[2] - 0x80) << 6)
| (unsigned int)(p[3] - 0x80);
unsigned int channel = offset / 256;
uint8_t value = (uint8_t)(offset % 256);
bool bFG = (channel < 3);
// Seed R, G, B from the preceding palette base.
//
int palIdx = bFG ? lastFGIdx : lastBGIdx;
uint8_t r, g, b;
if (0 <= palIdx && palIdx < 256)
{
r = palette[palIdx].rgb.r;
g = palette[palIdx].rgb.g;
b = palette[palIdx].rgb.b;
}
else
{
r = 0; g = 0; b = 0;
}
// Apply the first channel delta.
//
unsigned int ch_offset = bFG ? channel : channel - 3;
if (0 == ch_offset) { r = value; }
else if (1 == ch_offset) { g = value; }
else { b = value; }
p += 4;
// Look ahead for more channel codes in the same layer.
//
while ( 0xF3 == p[0]
&& 0xB0 == p[1]
&& p[2] >= 0x80 && p[2] <= 0x97
&& p[3] >= 0x80 && p[3] <= 0xBF)
{
unsigned int next_offset = ((unsigned int)(p[2] - 0x80) << 6)
| (unsigned int)(p[3] - 0x80);
unsigned int next_channel = next_offset / 256;
uint8_t next_value = (uint8_t)(next_offset % 256);
bool next_fg = (next_channel < 3);
if (next_fg != bFG) break;
unsigned int next_ch_offset = bFG ? next_channel : next_channel - 3;
if (0 == next_ch_offset) { r = next_value; }
else if (1 == next_ch_offset) { g = next_value; }
else { b = next_value; }
p += 4;
}
// Emit new 2-code-point encoding (8 bytes).
// The output can be larger than input (1 delta = 4 bytes → 8 bytes),
// but we checked capacity above.
//
unsigned int base_block = bFG ? 0 : 2;
unsigned int cp1_payload = ((unsigned int)(r >> 4) << 8) | g;
unsigned int cp2_payload = ((unsigned int)(r & 0xF) << 8) | b;
q[0] = 0xF3;
q[1] = (UTF8)(0xB0 + base_block);
q[2] = (UTF8)(0x80 | ((cp1_payload >> 6) & 0x3F));
q[3] = (UTF8)(0x80 | (cp1_payload & 0x3F));
q += 4;
q[0] = 0xF3;
q[1] = (UTF8)(0xB0 + base_block + 1);
q[2] = (UTF8)(0x80 | ((cp2_payload >> 6) & 0x3F));
q[3] = (UTF8)(0x80 | (cp2_payload & 0x3F));
q += 4;
bChanged = true;
}
else
{
*q++ = *p++;
}
}
*q = '\0';
*pnNew = (size_t)(q - pNew);
return bChanged;
}
// The following mux_AttrNameInitialSet_latin1 is only used for converting
// A_LOCK.
//
// The first character of an attribute name must be either alphabetic,
// '_', '#', '.', or '~'. It's handled by the following table.
//
// Characters thereafter may be letters, numbers, and characters from
// the set {'?!`/-_.@#$^&~=+<>()}. Lower-case letters are turned into
// uppercase before being used, but lower-case letters are valid input.
//
static bool mux_AttrNameInitialSet_latin1[256] =
{
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
//
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 5
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 7
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, // 9
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, // A
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, // B
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // C
1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, // D
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // E
1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0 // F
};
/* ---------------------------------------------------------------------------
* getboolexp1: Get boolean subexpression from file.
*
* This is only used to import v2 flatfiles.
*/
// Set true when getboolexp1() hits a malformed/over-nested lock while importing
// a v1/v2 flatfile. Lets the recursion unwind without crashing and lets the
// caller (db_read) abort the load cleanly (#806 sibling): the runtime @lock
// parser caps nesting at lock_nest_lim, but this import path was unguarded, so a
// corrupt/malicious v2 flatfile could blow the stack (deeply-nested lock ->
// SIGSEGV) or trip a mux_assert (truncated/garbage lock -> SIGABRT).
//
static bool s_boolexp_corrupt = false;
// Recursion bound for the import-time lock parser. Far above any legitimate
// lock (the runtime parser caps nesting at lock_nest_lim, default 20) yet far
// below the depth that would overflow the stack.
//
static const int BOOLEXP_LOAD_NEST_MAX = 1024;
static BOOLEXP *getboolexp1(FILE *f, int depth)
{
BOOLEXP *b;
UTF8 *s;
int d;
if (depth > BOOLEXP_LOAD_NEST_MAX)
{
// Over-nested lock: stop recursing before the stack overflows.
//
s_boolexp_corrupt = true;
return TRUE_BOOLEXP;
}
int c = getc(f);
switch (c)
{
case '\n':
ungetc(c, f);
return TRUE_BOOLEXP;
case EOF:
// Unexpected EOF in boolexp.
//
goto error;
case '(':
b = alloc_bool("getboolexp1.openparen");
switch (c = getc(f))
{
case NOT_TOKEN:
b->type = BOOLEXP_NOT;
b->sub1 = getboolexp1(f, depth + 1);
break;
case INDIR_TOKEN:
b->type = BOOLEXP_INDIR;
b->sub1 = getboolexp1(f, depth + 1);
break;
case IS_TOKEN:
b->type = BOOLEXP_IS;
b->sub1 = getboolexp1(f, depth + 1);
break;
case CARRY_TOKEN:
b->type = BOOLEXP_CARRY;
b->sub1 = getboolexp1(f, depth + 1);
break;
case OWNER_TOKEN:
b->type = BOOLEXP_OWNER;
b->sub1 = getboolexp1(f, depth + 1);
break;
default:
ungetc(c, f);
b->sub1 = getboolexp1(f, depth + 1);
if ('\n' == (c = getc(f)))
{
c = getc(f);
}
switch (c)
{
case AND_TOKEN:
b->type = BOOLEXP_AND;
break;
case OR_TOKEN:
b->type = BOOLEXP_OR;
break;
default:
// Bad connective: b->sub1 (built above) and the node leak
// otherwise. b->type is not yet set, so free sub1 explicitly
// rather than via free_boolexp(b).
//
free_boolexp(b->sub1);
free_bool(b);
goto error;
}
b->sub2 = getboolexp1(f, depth + 1);
}
if ('\n' == (d = getc(f)))
{
d = getc(f);
}
if (')' != d)
{
// Missing close paren: b->type and its subtree(s) are fully set
// here, so free_boolexp() unwinds them correctly.
//
free_boolexp(b);
goto error;
}
return b;
default: // dbref or attribute.
ungetc(c, f);
b = alloc_bool("getboolexp1.default");
b->type = BOOLEXP_CONST;
b->thing = 0;
// This is either an attribute, eval, or constant lock. Constant locks
// are of the form <num>, while attribute and eval locks are of the
// form <anam-or-anum>:<string> or <aname-or-anum>/<string>
// respectively. The characters <nl>, |, and & terminate the string.
//
if (mux_isdigit(c))
{
while (mux_isdigit(c = getc(f)))
{
b->thing = b->thing * 10 + c - '0';
}
}
else if (mux_AttrNameInitialSet_latin1[static_cast<unsigned char>(c)])
{
LBuf buff = LBuf_Src("getboolexp1.atr_name");
s = buff.get();
while ( EOF != (c = getc(f))
&& '\n' != c
&& ':' != c
&& '/' != c
&& s < buff.get() + LBUF_SIZE - 1)
{
*s++ = static_cast<UTF8>(c);
}
if (EOF == c)
{
free_bool(b);
goto error;
}
*s = '\0';
// Look the name up as an attribute. If not found, create a new
// attribute.
//
int anum = mkattr(GOD, buff);
if (anum <= 0)
{
free_bool(b);
goto error;
}
b->thing = anum;
}
else
{
free_bool(b);
goto error;
}
// If last character is : then this is an attribute lock. A last
// character of / means an eval lock.
//
if ( ':' == c
|| '/' == c)
{
if ('/' == c)
{
b->type = BOOLEXP_EVAL;
}
else
{
b->type = BOOLEXP_ATR;
}
LBuf buff = LBuf_Src("getboolexp1.attr_lock");
s = buff.get();
while ( EOF != (c = getc(f))
&& '\n' != c
&& ')' != c
&& OR_TOKEN != c
&& AND_TOKEN != c
&& s < buff.get() + LBUF_SIZE - 1)
{
*s++ = static_cast<UTF8>(c);
}
if (EOF == c)
{
// EOF mid lock-string: b->sub1 not yet allocated (StringClone
// below is unreached), so free only the node.
//
free_bool(b);
goto error;
}
*s = '\0';
b->sub1 = reinterpret_cast<BOOLEXP *>(StringClone(buff));
}
ungetc(c, f);
return b;
}
error:
// Malformed lock (EOF mid-expression, bad syntax, or over-nesting). Flag
// the corruption and unwind without crashing; getboolexp()/db_read abort
// the load cleanly instead of mux_assert-ing.
//
s_boolexp_corrupt = true;
return TRUE_BOOLEXP;
}
/* ---------------------------------------------------------------------------
* getboolexp: Read a boolean expression from the flat file.
*
* This is only used to import v2 flatfile.
*/
static BOOLEXP *getboolexp(FILE *f)
{
s_boolexp_corrupt = false;
BOOLEXP *b = getboolexp1(f, 0);
int c = getc(f);
if (c != '\n')
{
// Malformed lock: the expression was not terminated by a newline.
//
s_boolexp_corrupt = true;
}
if ((c = getc(f)) != '\n')
{
ungetc(c, f);
}
return b;
}
int g_max_nam_atr = INT_MIN;
int g_max_obj_atr = INT_MIN;
/* ---------------------------------------------------------------------------
* get_list: Read attribute list from flat file.
*/
static bool get_list(FILE *f, dbref i)
{
LBuf buff = LBuf_Src("get_list");
for (;;)
{
dbref atr;
int c;
switch (c = getc(f))
{
case '>': // read # then string
atr = getref(f);
if (atr > 0)
{
// Maximum attribute number across all objects.
//
if (g_max_obj_atr < atr)
{
g_max_obj_atr = atr;
}
size_t nBufferUnicode;
UTF8 *pBufferUnicode;
if (3 <= g_version)
{
pBufferUnicode = reinterpret_cast<UTF8 *>(getstring_noalloc(f, true, &nBufferUnicode));
}
else
{
size_t nBufferLatin1;
char *pBufferLatin1 = reinterpret_cast<char *>(getstring_noalloc(f, true, &nBufferLatin1));
pBufferUnicode = ConvertToUTF8(pBufferLatin1, &nBufferUnicode);
}
// Ignore legacy packed attribute-list payload.
//
if (atr != A_LIST)
{
if (!atr_add_raw_LEN(i, atr, pBufferUnicode, nBufferUnicode))
{
Log.tinyprintf(T("Failed writing attribute %d on object #%d during flatfile import." ENDLINE), atr, i);
return false;
}
}
}
else
{
// Silently discard
//
size_t nBuffer;
(void)getstring_noalloc(f, true, &nBuffer);
}
break;
case '\n': // ignore newlines. They're due to v(r).
break;
case '<': // end of list
c = getc(f);
if (c != '\n')
{
ungetc(c, f);
Log.tinyprintf(T("No line feed on object %d" ENDLINE), i);
return true;
}
return true;
case EOF:
// Truncated flatfile: the attribute list was cut off before its
// '<' terminator. Abort the load rather than spinning forever —
// getstring_noalloc() makes no progress at EOF, so the default
// case below would loop indefinitely emitting log lines.
//
Log.tinyprintf(T("Unexpected EOF when getting attributes on object %d" ENDLINE), i);
return false;
default:
Log.tinyprintf(T("Bad character %c when getting attributes on object %d" ENDLINE), c, i);
// We've found a bad spot. I hope things aren't too bad.
//
{
size_t nBuffer;
(void)getstring_noalloc(f, true, &nBuffer);
}
}
}
}
dbref db_read(FILE *f, int *db_format, int *db_version, int *db_flags)
{
dbref i, anum;
int ch;
const UTF8 *tstr;
int aflags;
BOOLEXP *tempbool;
UTF8 *buff;
size_t nBuffer;
g_format = F_UNKNOWN;
g_version = 0;
g_flags = 0;
g_max_nam_atr = INT_MIN;
g_max_obj_atr = INT_MIN;
bool header_gotten = false;
bool size_gotten = false;
bool nextattr_gotten = false;
bool convert_values = false;
bool read_attribs = true;
bool read_name = true;
bool read_key = true;
bool read_money = true;
size_t nName;
bool bValid;
UTF8 *pName;
int iDotCounter = 0;
if (mudstate.bStandAlone)
{
Log.WriteString(T("Reading "));
Log.Flush();
}
db_free();
for (i = 0;; i++)
{
if (mudstate.bStandAlone)
{
if (!iDotCounter)
{
iDotCounter = 100;
fputc('.', stderr);
fflush(stderr);
}
iDotCounter--;
}
ch = getc(f);
switch (ch)
{
case '-': // Misc tag
ch = getc(f);
if (ch == 'R')
{
// Record number of players
//
mudstate.record_players = getref(f);
if (mudconf.reset_players)
{
mudstate.record_players = 0;
}
}
break;
case '+':
// MUX header
//
ch = getc(f);
if (ch == 'A')
{
// USER-NAMED ATTRIBUTE
//
anum = getref(f);
// Validate the user-attribute number before it reaches
// g_max_nam_atr (which seeds attr_next) or vattr_define_LEN ->
// anum_set/anum_extend (#808). A negative number from a
// corrupt/malicious flatfile would OOB-write anum_table; a huge
// one would set attr_next enormous (runtime OOM) and make
// anum_extend allocate a giant table. User attribute numbers
// are always >= A_USER_START. Skip the bad record (still drain
// its value string) so the load degrades gracefully rather than
// corrupting memory.
//
if ( anum < A_USER_START
|| anum > A_USER_MAX)
{
Log.tinyprintf(T(ENDLINE "db_read: user-attribute number #%d is out of range; skipping a corrupt flatfile +A record." ENDLINE), anum);
size_t nUnused;
(void)getstring_noalloc(f, true, &nUnused);
break;
}
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, true, &nBuffer));
if (mux_isdigit(*tstr))
{
aflags = 0;
while (mux_isdigit(*tstr))
{
aflags = (aflags * 10) + (*tstr++ - '0');
}
tstr++; // skip ':'
}
else
{
aflags = mudconf.vattr_flags;
}
// If v2 flatfile or earlier, convert tstr to UTF-8.
//
if (g_version <= 2)
{
size_t nUnused;
tstr = ConvertToUTF8(reinterpret_cast<const char *>(tstr), &nUnused);
}
pName = MakeCanonicalAttributeName(tstr, &nName, &bValid);
if (bValid)
{
// Maximum attribute number across all names.
//
if (g_max_nam_atr < anum)
{
g_max_nam_atr = anum;
}
vattr_define_LEN(pName, nName, anum, aflags);
}
}
else if (ch == 'X')
{
// MUX VERSION
//
if (header_gotten)
{
Log.tinyprintf(T(ENDLINE "Duplicate MUX version header entry at object %d, ignored." ENDLINE), i);
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, false, &nBuffer));
}
else
{
header_gotten = true;
g_format = F_MUX;
g_version = getref(f);
g_flags = g_version & ~V_MASK;
g_version &= V_MASK;
if ( g_version < MIN_SUPPORTED_VERSION
|| MAX_SUPPORTED_VERSION < g_version)
{
Log.tinyprintf(T(ENDLINE "Unsupported flatfile version: %d." ENDLINE), g_version);
return -1;
}
// Due to potential UTF-8 characters in attribute names,
// we do not support parsing A_LOCK from the header. After
// converting from v2 to v4, this should never be needed
// anyway.
//
mux_assert( ( ( 1 == g_version
|| 2 == g_version)
&& (g_flags & MANDFLAGS_V2) == MANDFLAGS_V2)
|| ( 3 == g_version
&& (g_flags & MANDFLAGS_V3) == MANDFLAGS_V3)
|| ( 4 == g_version
&& (g_flags & MANDFLAGS_V4) == MANDFLAGS_V4)
|| ( 5 == g_version
&& (g_flags & MANDFLAGS_V5) == MANDFLAGS_V5));
// Otherwise extract feature flags
//
if (g_flags & V_DATABASE)
{
if ( 1 == g_version
|| 2 == g_version)
{
// We'll convert the external database from
// Latin-1 to UTF-8 at the end.
//
convert_values = true;
}
read_attribs = false;
read_name = !(g_flags & V_ATRNAME);
}
read_key = !(g_flags & V_ATRKEY);
read_money = !(g_flags & V_ATRMONEY);
}
}
else if (ch == 'S')
{
// SIZE
//
if (size_gotten)
{
Log.tinyprintf(T(ENDLINE "Duplicate size entry at object %d, ignored." ENDLINE), i);
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, false, &nBuffer));
}
else
{
mudstate.min_size = getref(f);
size_gotten = true;
}
}
else if (ch == 'N')
{
// NEXT ATTR TO ALLOC WHEN NO FREELIST
//
if (nextattr_gotten)
{
Log.tinyprintf(T(ENDLINE "Duplicate next free vattr entry at object %d, ignored." ENDLINE), i);
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, false, &nBuffer));
}
else
{
mudstate.attr_next = getref(f);
nextattr_gotten = true;
}
}
else
{
Log.tinyprintf(T(ENDLINE "Unexpected character %c in MUX header near object #%d, ignored." ENDLINE), ch, i);
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, false, &nBuffer));
}
break;
case '!': // MUX entry
i = getref(f);
// Validate the object dbref before using it to index db[] or to
// size db_grow() (#806). getref() returns an unchecked mux_atoi64()
// result, and s_Name()/s_Location()/etc. write db[i] with no bounds
// check (SIZE_HACK == 1, so only db[-1] is valid). A corrupt or
// malicious flatfile can therefore carry a negative dbref (OOB write
// into db[] -> SIGSEGV / heap corruption) or an enormous one
// (db_grow's MEMALLOC fails -> mux_assert abort). The SQLite load
// path already validates this (sqlite_load_game, db.cpp); mirror it
// here. DB_LOAD_MAX_DBREF is far above any real game (init_size
// defaults to 1000; the largest MUX databases are a few million
// objects), so a legitimate flatfile never trips it, while a
// clearly-garbage dbref aborts the load cleanly (return -1; the
// caller rolls back the in-progress SQLite import).
//
{
const dbref DB_LOAD_MAX_DBREF = 0x10000000; // 268,435,456
if (i < 0 || i > DB_LOAD_MAX_DBREF)
{
Log.tinyprintf(T(ENDLINE "db_read: object dbref #%d is out of range; aborting load of a corrupt or malicious flatfile." ENDLINE), i);
return -1;
}
}
db_grow(i + 1);
if (read_name)
{
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, true, &nBuffer));
if (g_version <= 2)
{
size_t nUsed;
tstr = ConvertToUTF8(reinterpret_cast<const char *>(tstr), &nUsed);
}
buff = alloc_mbuf("dbread.s_Name");
StripTabsAndTruncate(tstr, buff, MBUF_SIZE-1, MBUF_SIZE-1);
s_Name(i, buff);
free_mbuf(buff);
s_Location(i, getref(f));
}
else
{
s_Location(i, getref(f));
}
// ZONE
//
int zone;
zone = getref(f);
if (zone < NOTHING)
{
zone = NOTHING;
}
s_Zone(i, zone);
// CONTENTS and EXITS
//
s_Contents(i, getref(f));
s_Exits(i, getref(f));
// LINK
//
s_Link(i, getref(f));
// NEXT
//
s_Next(i, getref(f));
// LOCK
//
if (read_key)
{
// Parse lock directly from flatfile.
// Only used when reading v2 format.
//
tempbool = getboolexp(f);
if (s_boolexp_corrupt)
{
// Malformed / over-nested lock in the flatfile (#806
// sibling): abort the load cleanly rather than crash.
//
free_boolexp(tempbool);
Log.tinyprintf(T(ENDLINE "db_read: malformed or over-nested lock for object #%d; aborting load of a corrupt or malicious flatfile." ENDLINE), i);
return -1;
}
if (!atr_add_raw(i, A_LOCK, unparse_boolexp_quiet(1, tempbool)))
{
free_boolexp(tempbool);
Log.tinyprintf(T(ENDLINE "Error writing lock for object #%d" ENDLINE), i);
return -1;
}
free_boolexp(tempbool);
}
// OWNER
//
s_Owner(i, getref(f));
// PARENT
//
s_Parent(i, getref(f));
// PENNIES
//
if (read_money)
{
s_PenniesDirect(i, getref(f));
}
// FLAGS
//
s_Flags(i, FLAG_WORD1, getref(f));
s_Flags(i, FLAG_WORD2, getref(f));
s_Flags(i, FLAG_WORD3, getref(f));
// POWERS
//
s_Powers(i, getref(f));
s_Powers2(i, getref(f));
// ATTRIBUTES
//
if (read_attribs)
{
if (!get_list(f, i))
{
Log.tinyprintf(T(ENDLINE "Error reading attrs for object #%d" ENDLINE), i);
return -1;
}
}
// check to see if it's a player
//
if (isPlayer(i))
{
c_Connected(i);
}
break;
case '*': // EOF marker
tstr = reinterpret_cast<UTF8 *>(getstring_noalloc(f, false, &nBuffer));
if (strncmp(reinterpret_cast<const char *>(tstr), "**END OF DUMP***", 16))
{
Log.tinyprintf(T(ENDLINE "Bad EOF marker at object #%d" ENDLINE), i);
return -1;
}
else
{
// Attribute number warnings.
//
if (g_max_nam_atr < g_max_obj_atr)
{
Log.tinyprintf(T(ENDLINE "Warning: One or more attribute values are unnamed. Did you use ./Backup on a running game?"));
}
if (!nextattr_gotten)
{
Log.tinyprintf(T(ENDLINE "Warning: Missing +N<next free>. Adjusting."));
}
if (mudstate.attr_next <= g_max_nam_atr)
{
if (nextattr_gotten)
{
Log.tinyprintf(T(ENDLINE "Warning: +N<next free attr> conflicts with existing attribute names. Adjusting."));
}
mudstate.attr_next = g_max_nam_atr + 1;
}
if (mudstate.attr_next <= g_max_obj_atr)
{
if (nextattr_gotten)
{
Log.tinyprintf(T(ENDLINE "Warning: +N<next free attr> conflicts object attribute numbers. Adjusting."));
}
mudstate.attr_next = g_max_nam_atr + 1;
}
int max_atr = A_USER_START;
if (max_atr < g_max_nam_atr)
{
max_atr = g_max_nam_atr;
}
if (max_atr < g_max_obj_atr)
{
max_atr = g_max_obj_atr;
}
if (max_atr + 1 < mudstate.attr_next)
{
if (nextattr_gotten)
{
Log.tinyprintf(T(ENDLINE "Info: +N<next free attr> can be safely adjusted down."));
}
mudstate.attr_next = max_atr + 1;
}
if (convert_values)
{
Log.WriteString(T("Converting external database to UTF-8 " ENDLINE));
Log.Flush();
// Convert every attribute on every object in the external database.
//
dbref iObject;
atr_push();
DO_WHOLE_DB(iObject)
{
unsigned char *as;
for (int iAttr = atr_head(iObject, &as); iAttr; iAttr = atr_next(&as))
{
if ( 0 < iAttr
&& iAttr <= anum_alc_top)
{
const char *pLatin1 = reinterpret_cast<const char *>(atr_get_raw(iObject, iAttr));
if (nullptr != pLatin1)
{
size_t nUnicode;
const UTF8 *pUnicode = ConvertToUTF8(pLatin1, &nUnicode);
if (!atr_add_raw_LEN(iObject, iAttr, pUnicode, nUnicode))
{
Log.tinyprintf(T("Failed writing converted attribute %d on object #%d." ENDLINE), iAttr, iObject);
atr_pop();
return -1;
}
}
}
}
}
atr_pop();
}
// Migrate V4 PUA color encoding to V5 if needed.
//
if (g_version <= 4)
{
Log.WriteString(T("Migrating V4 24-bit color encoding to V5 " ENDLINE));
Log.Flush();
LBuf pMigBuf = LBuf_Src("MigrateColorV4toV5");
dbref iObject;
atr_push();
DO_WHOLE_DB(iObject)
{
unsigned char *as;
for (int iAttr = atr_head(iObject, &as); iAttr; iAttr = atr_next(&as))
{
if ( 0 < iAttr
&& iAttr <= anum_alc_top)
{
const UTF8 *pRaw = atr_get_raw(iObject, iAttr);
if (nullptr != pRaw)
{
size_t nNew;
if (MigrateColorV4toV5(pRaw, pMigBuf, LBUF_SIZE, &nNew))
{
atr_add_raw_LEN(iObject, iAttr, pMigBuf, nNew);
}
}
}
}
}
atr_pop();
}
*db_version = g_version;
*db_format = g_format;
*db_flags = g_flags;
if (mudstate.bStandAlone)
{
Log.WriteString(T(ENDLINE));
Log.Flush();
}
else
{
load_player_names();
}
return mudstate.db_top;
}
case EOF:
Log.tinyprintf(T(ENDLINE "Unexpected end of file near object #%d" ENDLINE), i);
return -1;
default:
if (mux_isprint_ascii(ch))
{
Log.tinyprintf(T(ENDLINE "Illegal character %c near object #%d" ENDLINE), ch, i);
}
else
{
Log.tinyprintf(T(ENDLINE "Illegal character 0x%02x near object #%d" ENDLINE), ch, i);
}
return -1;
}
}
}
// #1869: flatfile writer must observe stream errors; putref/putstring/
// fwrite discard return values, so ferror() after each logical record is
// the reliable check. Returns true while the stream is still good.
//
static bool db_write_stream_ok(FILE *f)
{
return nullptr != f && 0 == ferror(f);
}
// Returns true if the object was fully written; false on stream error.
//
static bool db_write_object(FILE *f, dbref i, int db_format, int flags)
{
UNUSED_PARAMETER(db_format);
ATTR *a;
int ca, j;
if (!(flags & V_ATRNAME))
{
putstring(f, Name(i));
}
putref(f, Location(i));
putref(f, Zone(i));
putref(f, Contents(i));
putref(f, Exits(i));
putref(f, Link(i));
putref(f, Next(i));
putref(f, Owner(i));
putref(f, Parent(i));
if (!(flags & V_ATRMONEY))
{
putref(f, Pennies(i));
}
putref(f, Flags(i));
putref(f, Flags2(i));
putref(f, Flags3(i));
putref(f, Powers(i));
putref(f, Powers2(i));
// Write the attribute list.
//
if (!(flags & V_DATABASE))
{
UTF8 buf[SBUF_SIZE];
buf[0] = '>';
UTF8 *abuf = alloc_lbuf("db_write_object.attr");
UTF8 *ebuf = alloc_lbuf("db_write_object.enc");
unsigned char *as;
for (ca = atr_head(i, &as); ca; ca = atr_next(&as))
{
if (mudstate.bStandAlone)
{
j = ca;
}
else
{
a = atr_num(ca);
if (!a)
{
continue;
}
j = a->number;
}
if (j < A_USER_START)
{
switch (j)
{
case A_NAME:
if (!(flags & V_ATRNAME))
{
continue;
}
break;
case A_MONEY:
continue;
}
}
// Format is: ">%d\n", j
//
// atr_get_raw() returns the attribute text with the
// \x01owner:flags: prefix already stripped by the cache
// layer, so per-attribute owner overrides and flags
// (AF_NOEVAL, AF_LOCK, ...) must be re-encoded here or a
// flatfile export silently loses them.
//
dbref aowner;
int aflags;
size_t nText;
atr_get_str_LEN(abuf, i, j, &aowner, &aflags, &nText);
size_t n = mux_ltoa(j, buf+1) + 1;
buf[n++] = '\n';
fwrite(buf, sizeof(UTF8), n, f);
if (((aowner == Owner(i)) || (aowner == NOTHING)) && !aflags)
{
putstring(f, abuf);
}
else
{
if (aowner == NOTHING)
{
aowner = Owner(i);
}
mux_sprintf(ebuf, LBUF_SIZE, T("%c%d:%d:%s"), ATR_INFO_CHAR,
aowner, aflags, abuf);
putstring(f, ebuf);
}
if (!db_write_stream_ok(f))
{
free_lbuf(abuf);
free_lbuf(ebuf);
return false;
}
}
free_lbuf(abuf);
free_lbuf(ebuf);
fwrite("<\n", sizeof(UTF8), 2, f);
}
return db_write_stream_ok(f);
}
// Returns mudstate.db_top on success, -1 on format/I/O failure (#1869).
// Callers must not treat a negative return as a successful dump.
//
dbref db_write(FILE *f, int format, int version)
{
dbref i;
int flags;
ATTR *vp;
switch (format)
{
case F_MUX:
flags = version;
break;
default:
Log.WriteString(T("Can only write MUX format." ENDLINE));
return -1;
}
if (mudstate.bStandAlone)
{
Log.WriteString(T("Writing "));
Log.Flush();
}
i = mudstate.attr_next;
mux_fprintf(f, T("+X%d\n+S%d\n+N%d\n"), flags, mudstate.db_top, i);
mux_fprintf(f, T("-R%d\n"), mudstate.record_players);
if (!db_write_stream_ok(f))
{
Log.WriteString(T("Flatfile write failed while writing header." ENDLINE));
return -1;
}
// Dump user-named attribute info.
//
LBuf Buffer = LBuf_Src("db_write_vattrs");
Buffer[0] = '+';
Buffer[1] = 'A';
int iAttr;
for (iAttr = A_USER_START; iAttr <= anum_alc_top; iAttr++)
{
vp = static_cast<ATTR *>(anum_get(iAttr));
if ( vp != nullptr
&& !(vp->flags & AF_DELETED))
{
// Format is: "+A%d\n\"%d:%s\"\n", vp->number, vp->flags, vp->name
//
UTF8 *pBuffer = Buffer+2;
pBuffer += mux_ltoa(vp->number, pBuffer);
*pBuffer++ = '\n';
*pBuffer++ = '"';
pBuffer += mux_ltoa(vp->flags, pBuffer);
*pBuffer++ = ':';
size_t nNameLength = strlen(reinterpret_cast<const char *>(vp->name));
memcpy(pBuffer, vp->name, nNameLength);
pBuffer += nNameLength;
*pBuffer++ = '"';
*pBuffer++ = '\n';
fwrite(Buffer, sizeof(UTF8), pBuffer-Buffer, f);
if (!db_write_stream_ok(f))
{
Log.WriteString(T("Flatfile write failed while writing vattrs." ENDLINE));
return -1;
}
}
}
int iDotCounter = 0;
UTF8 buf[SBUF_SIZE];
buf[0] = '!';
DO_WHOLE_DB(i)
{
if (mudstate.bStandAlone)
{
if (!iDotCounter)
{
iDotCounter = 100;
fputc('.', stderr);
fflush(stderr);
}
iDotCounter--;
}
if (!isGarbage(i))
{
// Format is: "!%d\n", i
//
size_t n = mux_ltoa(i, buf+1) + 1;
buf[n++] = '\n';
fwrite(buf, sizeof(UTF8), n, f);
if ( !db_write_stream_ok(f)
|| !db_write_object(f, i, format, flags))
{
if (mudstate.bStandAlone)
{
Log.WriteString(T(ENDLINE "Flatfile write failed." ENDLINE));
}
else
{
STARTLOG(LOG_PROBLEMS, "DMP", "FAIL");
log_printf(T("Flatfile write failed while writing object #%d."), i);
ENDLOG;
}
return -1;
}
}
}
if (EOF == fputs("***END OF DUMP***\n", f))
{
if (mudstate.bStandAlone)
{
Log.WriteString(T(ENDLINE "Flatfile write failed at end marker." ENDLINE));
}
return -1;
}
// Ensure buffered data is pushed and re-check the stream before claiming
// a complete dump (#1869).
//
if (0 != fflush(f) || !db_write_stream_ok(f))
{
if (mudstate.bStandAlone)
{
Log.WriteString(T(ENDLINE "Flatfile write failed on flush." ENDLINE));
}
else
{
STARTLOG(LOG_PROBLEMS, "DMP", "FAIL");
log_text(T("Flatfile write failed on flush."));
ENDLOG;
}
return -1;
}
if (mudstate.bStandAlone)
{
Log.WriteString(T(ENDLINE));
Log.Flush();
}
return mudstate.db_top;
}