mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
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.
1349 lines
42 KiB
C++
1349 lines
42 KiB
C++
/*! \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;
|
||
}
|