2026-03-04 21:08:06 -07:00
|
|
|
/*! \file sqlite_backend.cpp
|
2026-03-04 21:23:24 -07:00
|
|
|
* \brief SQLite implementation of IStorageBackend.
|
2026-03-04 21:08:06 -07:00
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-04 21:23:24 -07:00
|
|
|
#if !defined(TINYMUX_TYPES_DEFINED)
|
2026-03-04 21:08:06 -07:00
|
|
|
#include "copyright.h"
|
|
|
|
|
#include "autoconf.h"
|
|
|
|
|
#include "config.h"
|
2026-03-04 21:23:24 -07:00
|
|
|
#include "externs.h"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#include "sqlite_backend.h"
|
2026-07-31 09:06:07 -06:00
|
|
|
#include <cstring>
|
2026-03-04 21:23:24 -07:00
|
|
|
|
|
|
|
|
CSQLiteBackend::CSQLiteBackend()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CSQLiteBackend::~CSQLiteBackend()
|
|
|
|
|
{
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::Open(const char *path)
|
|
|
|
|
{
|
|
|
|
|
return m_db.Open(path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CSQLiteBackend::Close()
|
|
|
|
|
{
|
|
|
|
|
m_db.Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::IsOpen() const
|
|
|
|
|
{
|
|
|
|
|
return m_db.IsOpen();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::Get(unsigned int object, unsigned int attrnum,
|
2026-03-05 08:09:38 -07:00
|
|
|
UTF8 *buf, size_t buflen, size_t *pLen,
|
|
|
|
|
int *owner, int *flags)
|
2026-03-04 21:23:24 -07:00
|
|
|
{
|
2026-03-05 08:09:38 -07:00
|
|
|
dbref db_owner;
|
|
|
|
|
int db_flags;
|
|
|
|
|
bool rc = m_db.GetAttribute(static_cast<dbref>(object), static_cast<int>(attrnum),
|
|
|
|
|
buf, buflen, pLen, &db_owner, &db_flags);
|
|
|
|
|
if (rc)
|
|
|
|
|
{
|
2026-03-05 23:11:03 -07:00
|
|
|
if (owner) *owner = static_cast<int>(db_owner);
|
|
|
|
|
if (flags) *flags = db_flags;
|
2026-03-05 08:09:38 -07:00
|
|
|
}
|
|
|
|
|
return rc;
|
2026-03-04 21:23:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::Put(unsigned int object, unsigned int attrnum,
|
2026-03-05 08:09:38 -07:00
|
|
|
const UTF8 *value, size_t len,
|
|
|
|
|
int owner, int flags)
|
2026-03-04 21:23:24 -07:00
|
|
|
{
|
|
|
|
|
return m_db.PutAttribute(static_cast<dbref>(object), static_cast<int>(attrnum),
|
2026-03-05 08:09:38 -07:00
|
|
|
value, len, static_cast<dbref>(owner), flags);
|
2026-03-04 21:23:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::Del(unsigned int object, unsigned int attrnum)
|
|
|
|
|
{
|
|
|
|
|
return m_db.DelAttribute(static_cast<dbref>(object), static_cast<int>(attrnum));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::DelAll(unsigned int object)
|
|
|
|
|
{
|
|
|
|
|
return m_db.DelAllAttributes(static_cast<dbref>(object));
|
|
|
|
|
}
|
2026-03-04 21:08:06 -07:00
|
|
|
|
2026-03-04 21:23:24 -07:00
|
|
|
bool CSQLiteBackend::GetAll(unsigned int object, AttrCallback cb)
|
|
|
|
|
{
|
|
|
|
|
return m_db.GetAllAttributes(static_cast<dbref>(object),
|
2026-03-05 08:09:38 -07:00
|
|
|
[&cb](int attrnum, const UTF8 *value, size_t len, dbref owner, int flags)
|
2026-03-04 21:23:24 -07:00
|
|
|
{
|
2026-03-05 08:09:38 -07:00
|
|
|
cb(static_cast<unsigned int>(attrnum), value, len,
|
|
|
|
|
static_cast<int>(owner), flags);
|
2026-03-04 21:23:24 -07:00
|
|
|
});
|
|
|
|
|
}
|
2026-03-04 21:08:06 -07:00
|
|
|
|
2026-03-05 08:34:03 -07:00
|
|
|
bool CSQLiteBackend::GetBuiltin(unsigned int object, AttrCallback cb)
|
|
|
|
|
{
|
|
|
|
|
return m_db.GetBuiltinAttributes(static_cast<dbref>(object),
|
|
|
|
|
[&cb](int attrnum, const UTF8 *value, size_t len, dbref owner, int flags)
|
|
|
|
|
{
|
|
|
|
|
cb(static_cast<unsigned int>(attrnum), value, len,
|
|
|
|
|
static_cast<int>(owner), flags);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
Complete Stage 2: backend interface audit
Story 2a: Extend IStorageBackend with Count(), GetModCount(), and
GetAllModCounts(). Implement in CSQLiteBackend. Route all attribute
access in attrcache.cpp and db.cpp through the interface, eliminating
GetDB() bypasses for attribute-specific operations.
Story 2b: New docs/attribute-metadata.md defines the per-attribute
metadata contract: (object, attrnum) -> (value, owner, flags, mod_count),
encoding requirements, mod_count semantics, and iteration guarantees.
Story 2c: Confirmed @search never queries the attributes table directly.
SQL fast-paths use object metadata only; eval= predicates go through
cache_get() -> IStorageBackend. No changes needed for backend swap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:22:22 -06:00
|
|
|
int CSQLiteBackend::Count(unsigned int object)
|
|
|
|
|
{
|
|
|
|
|
return m_db.CountAttributes(static_cast<dbref>(object));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t CSQLiteBackend::GetModCount(unsigned int object, unsigned int attrnum)
|
|
|
|
|
{
|
|
|
|
|
return m_db.GetAttrModCount(static_cast<dbref>(object),
|
|
|
|
|
static_cast<int>(attrnum));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CSQLiteBackend::GetAllModCounts(unsigned int object, ModCountCallback cb)
|
|
|
|
|
{
|
|
|
|
|
m_db.GetAllAttrModCounts(static_cast<dbref>(object),
|
|
|
|
|
[&cb](int attrnum, uint32_t mc)
|
|
|
|
|
{
|
|
|
|
|
cb(static_cast<unsigned int>(attrnum), mc);
|
|
|
|
|
});
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 21:23:24 -07:00
|
|
|
void CSQLiteBackend::Sync()
|
|
|
|
|
{
|
|
|
|
|
m_db.Checkpoint();
|
|
|
|
|
}
|
2026-03-04 21:08:06 -07:00
|
|
|
|
2026-03-04 21:23:24 -07:00
|
|
|
void CSQLiteBackend::Tick()
|
|
|
|
|
{
|
|
|
|
|
// Write-through means SQLite is always up to date.
|
|
|
|
|
// Periodic optimize is light maintenance.
|
|
|
|
|
//
|
|
|
|
|
m_db.Optimize();
|
|
|
|
|
}
|
fix(engine): bound the .sqlite path derivation (#1411)
Two sites derived the ".sqlite" sibling of mudconf.indb by open-coding
the same replace-or-append with strcpy/strcat, neither checking the
remaining capacity:
mux/modules/engine/engine_com.cpp (bMinDB RemoveFile) SIZEOF_PATHNAME
mux/modules/engine/attrcache.cpp (cache_init) LBUF_SIZE
input_database is cf_string_dyn with a maximum of SIZEOF_PATHNAME, so its
content can be SIZEOF_PATHNAME-1 characters. ".sqlite" is seven more plus
a terminator, so the engine_com buffer overflows in both branches: the
append writes eight bytes past a full buffer, and even the ".db" branch
that replaces three characters with seven needs four bytes it may not
have. attrcache was safe only because indb is capped well below
LBUF_SIZE -- an implicit cross-module invariant, not a check.
Replaced with one bounds-checked derive_sqlite_path() in sqlite_backend,
which both TUs already include. It returns false and leaves the buffer
untouched when the result will not fit; callers log and take a defined
path -- cache_init returns HF_OPEN_STATUS_ERROR after releasing the
backend it had just allocated, and the bMinDB case skips the removal,
since a path that cannot be formed names no file to remove. No new
abort: the failure is reported, not asserted.
Behaviour is unchanged for every input that fits, including the edge
cases the old `n > 3` test produced -- ".db" appends rather than
replaces, "a.dbx" and "db" append -- so this is not a silent change of
the derived name.
Verified with ASan on a standalone harness carrying both versions, an
input_database at exactly the configured maximum:
old, no .db suffix heap-buffer-overflow, WRITE of size 8
old, .db suffix heap-buffer-overflow, WRITE of size 8
new, both returns false, no diagnostic
and the normal cases: netmux.db -> netmux.sqlite, netmux ->
netmux.sqlite, data/netmux.db -> data/netmux.sqlite, plus the exact-fit
boundary accepted and one byte over rejected.
smoke 1489/1489 on both routes, 0 crashes, 314/314 dispatched.
Closes #1411.
2026-07-26 13:59:30 -06:00
|
|
|
|
|
|
|
|
bool derive_sqlite_path(char *buf, size_t buflen, const UTF8 *indb)
|
|
|
|
|
{
|
|
|
|
|
if ( nullptr == buf
|
|
|
|
|
|| 0 == buflen
|
|
|
|
|
|| nullptr == indb)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char suffix[] = ".sqlite";
|
|
|
|
|
const size_t nSuffix = sizeof(suffix) - 1;
|
|
|
|
|
|
|
|
|
|
const char *pIn = reinterpret_cast<const char *>(indb);
|
|
|
|
|
size_t nIn = strlen(pIn);
|
|
|
|
|
|
|
|
|
|
// A trailing ".db" is replaced rather than appended to, so the base is
|
|
|
|
|
// shorter than the input in that case. Note this still grows the path:
|
|
|
|
|
// three characters out, seven in.
|
|
|
|
|
//
|
|
|
|
|
size_t nBase = nIn;
|
|
|
|
|
if ( 3 < nIn
|
|
|
|
|
&& 0 == strcmp(pIn + nIn - 3, ".db"))
|
|
|
|
|
{
|
|
|
|
|
nBase = nIn - 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buflen < nBase + nSuffix + 1)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memcpy(buf, pIn, nBase);
|
|
|
|
|
memcpy(buf + nBase, suffix, nSuffix + 1);
|
|
|
|
|
return true;
|
|
|
|
|
}
|