/*! \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 , while attribute and eval locks are of the // form : or / // respectively. The characters , |, 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(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(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(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(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(getstring_noalloc(f, true, &nBufferUnicode)); } else { size_t nBufferLatin1; char *pBufferLatin1 = reinterpret_cast(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(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(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(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(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(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(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(getstring_noalloc(f, true, &nBuffer)); if (g_version <= 2) { size_t nUsed; tstr = ConvertToUTF8(reinterpret_cast(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(getstring_noalloc(f, false, &nBuffer)); if (strncmp(reinterpret_cast(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. Adjusting.")); } if (mudstate.attr_next <= g_max_nam_atr) { if (nextattr_gotten) { Log.tinyprintf(T(ENDLINE "Warning: +N 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 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 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(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(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(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; }