diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e4f1825f0..9feb1c62f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,6 +461,9 @@ endif() # These variables are defined: # LIBS_BASE, LIBS_ENGINE_BASE LIBS_ENGINE, LIBS_BASECLIENT, LIBS_CLIENT +add_library(srclibs-tomlc17 EXCLUDE_FROM_ALL ${TOMLC17LIST}) +set(LIBS_BASE ${LIBS_BASE} srclibs-tomlc17) + # Native client include(DaemonNacl) if (NACL) diff --git a/daemon.ini.template b/daemon.ini.template new file mode 100644 index 0000000000..cf1416adad --- /dev/null +++ b/daemon.ini.template @@ -0,0 +1,92 @@ +[gameinfo] +# Display name, can contain special characters, +# like "Unvanquished", "Smokin' Guns", etc. +name = "Unknown Dæmon game" +# Version string, usually in the form "0.52", "1.2.3", etc. +version = "0.0.1" +# XDG Application ID, used to by XDG desktops to associate +# windows of running binaries with their icons. +# It's written in the reverse-domain form +# like "net.unvanquished.Unvanquished". +appId = "com.example.Unknown" +# Directory base name where user data will be stored on Windows. +# For example with "Unvanquished" it will store data in +# C:\Users\\My Games\Unvanquished +# It's common to find capitalized names and even white spaces, +# but you may prefer to avoid special characters. +windowsDirName = "UnknownDaemonGame" +# Directory base name where user data will be stored on macOS. +# For example with "Unvanquished" it will store data in +# /Users//Library/Application Support/Unvanquished +# It's common to find capitalized names and even white spaces +# but you may prefer to avoid special characters. +# +# Some may prefer directory names in the form +# of "net.unvanquished.Unvanquished", this is not unusual. +macosDirName = "UnknownDaemonGame" +# Directory base name where user data will be stored on Linux +# and operating systems following freedesktop.org standards. +# +# For example with "unvanquished" it will store data in +# +# /home//.local/share/unvanquished +# +# The name is usually lowercase without space and without +# special characters. +# +# Some may prefer directory names in the form +# of “net.unvanquished.Unvanquished”, this is less common. +xdgDirName = "unknownDaemonGame" +# File base name for various files or directories written +# by the engine: screenshot base name, temporary file base name… +# +# For example with “unvanquished” it will name screenshot files +# the “unvanquished-.jpg way or create temporary files +# named like “/tmp/unvanquished-”. +baseName = "unknownDaemonGame" +# Base name of the package the engine should look for to start the game. +# For example with “unvanquished” the engine will look for a package named +# like this: +# +# unvanquished_.dpk +# +# Packages base names are usually lowercase, don't contain white spaces +# and cannot use “_” characters except for separating the base name +# and the version string. +basePak = "daemon" +# List of fully qualified domain name of the master servers. +# +# Example: ["master.unvanquished.net", "master2.unvanquished.net"] +# +# Up to five master servers are supported. +masterServers = ["master.example.com", "master2.example.com"] +# URL to download missing packages when joining a server. +# +# Example: "dl.unvanquished.net/pkg" +# Or: "dl.unvanquished.net:80/pkg" +# +# It is expected to be an http server. +# The protocol is omitted. +wwwBaseUrl = "dl.example.com/pkg" +# A string used to identify against the master server. +# +# In case of total conversion mods, this is the string of the +# game. +# +# It's usually upper case. +masterGameName = "UNKNOWN" +# A string used to filter games when listing servers from master +# servers. +# +# In case of total conversion mods, this is the string of the mod. +# Example: "unv" or "unvanquished" or "Unvanquished". +serverGameName = "unknown" +# A string used for game server urls. +# +# For example with "unv" you can have url in the form: +# +# unv://unvanquished.net +# +# It's lower case and usually short but you can use something longer +# similar to the base name. +uriProtocol = "unknown" diff --git a/libs/tomlc17/README.md b/libs/tomlc17/README.md new file mode 100644 index 0000000000..7752608466 --- /dev/null +++ b/libs/tomlc17/README.md @@ -0,0 +1,5 @@ +# tomlc17 + +Upstream: + +- https://github.com/cktan/tomlc17 diff --git a/libs/tomlc17/tomlc17.c b/libs/tomlc17/tomlc17.c new file mode 100644 index 0000000000..6f6f378c55 --- /dev/null +++ b/libs/tomlc17/tomlc17.c @@ -0,0 +1,2907 @@ +/* Copyright (c) 2024-2026, CK Tan. + * https://github.com/cktan/tomlc17/blob/main/LICENSE + */ +#include "tomlc17.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const toml_datum_t DATUM_ZERO = {0}; + +static toml_option_t toml_option = {0, realloc, free}; + +#define MALLOC(n) toml_option.mem_realloc(0, n) +#define REALLOC(p, n) toml_option.mem_realloc(p, n) +#define FREE(p) toml_option.mem_free(p) + +#define DO(x) \ + if (x) \ + return -1; \ + else \ + (void)0 + +// Copy string src to dst where dst is limited to dstsz that includes +// NUL. Return 0 on success, -1 otherwise (because src[] is longer than dst[]). +static inline int copystring(char *dst, int dstsz, const char *src) { + int srcsz = strlen(src) + 1; + if (srcsz > dstsz) { + return -1; + } + memcpy(dst, src, srcsz); + return 0; +} + +/* + * Error buffer + */ +typedef struct ebuf_t ebuf_t; +struct ebuf_t { + char *ptr; + int len; +}; + +/* + * Format an error into ebuf[]. Always return -1. + */ +static int SETERROR(ebuf_t ebuf, int lineno, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + char *p = ebuf.ptr; + char *q = p + ebuf.len; + if (lineno) { + snprintf(p, p < q ? q - p : 0, "(line %d) ", lineno); + p += strlen(p); + } + vsnprintf(p, p < q ? q - p : 0, fmt, args); + return -1; +} + +/* + * Memory pool. Allocated a big block once and hand out piecemeal. + */ +typedef struct pool_t pool_t; +struct pool_t { + int max; // size of buf[] + int top; // offset of first free byte in buf[] + char buf[1]; // first byte starts here +}; + +/** + * Create a memory pool of N bytes. Return the memory pool on + * success, or NULL if out of memory. + */ +static pool_t *pool_create(int N) { + if (N <= 0) { + N = 100; // minimum + } + int totalsz = sizeof(pool_t) + N; + pool_t *pool = MALLOC(totalsz); + if (!pool) { + return NULL; + } + memset(pool, 0, totalsz); + pool->max = N; + return pool; +} + +/** + * Destroy a memory pool. + */ +static void pool_destroy(pool_t *pool) { FREE(pool); } + +/** + * Allocate n bytes from pool. Return the memory allocated on + * success, or NULL if out of memory. + */ +static char *pool_alloc(pool_t *pool, int n) { + if (pool->top + n > pool->max) { + return NULL; + } + char *ret = pool->buf + pool->top; + pool->top += n; + return ret; +} + +/* This is a string view. */ +typedef struct span_t span_t; +struct span_t { + const char *ptr; + int len; +}; + +/* Represents a multi-part key */ +#define KEYPARTMAX 10 +typedef struct keypart_t keypart_t; +struct keypart_t { + int nspan; + span_t span[KEYPARTMAX]; +}; + +static int utf8_to_ucs(const char *s, int len, uint32_t *ret); +static int ucs_to_utf8(uint32_t code, char buf[4]); + +// flags for toml_datum_t::flag. +#define FLAG_INLINED 1 +#define FLAG_STDEXPR 2 +#define FLAG_EXPLICIT 4 + +// Maximum levels of brackets and braces to prevent +// stack overflow during recursive descent of the parser. +#define BRACKET_LEVEL_MAX 30 +#define BRACE_LEVEL_MAX 30 + +static inline size_t align8(size_t x) { return (((x) + 7) & ~7); } + +enum toktyp_t { + TOK_DOT = 1, + TOK_EQUAL, + TOK_COMMA, + TOK_LBRACK, + TOK_LLBRACK, + TOK_RBRACK, + TOK_RRBRACK, + TOK_LBRACE, + TOK_RBRACE, + TOK_LIT, + TOK_STRING, + TOK_MLSTRING, + TOK_LITSTRING, + TOK_MLLITSTRING, + TOK_TIME, + TOK_DATE, + TOK_DATETIME, + TOK_DATETIMETZ, + TOK_INTEGER, + TOK_FLOAT, + TOK_BOOL, + TOK_ENDL, + TOK_FIN = -5000, // EOF +}; +typedef enum toktyp_t toktyp_t; +typedef struct scanner_t scanner_t; + +/* Remember the current state of a scanner */ +typedef struct scanner_state_t scanner_state_t; +struct scanner_state_t { + scanner_t *sp; + const char *cur; // points into scanner_t::src[] + int lineno; // current line number +}; + +// A scan token +typedef struct token_t token_t; +struct token_t { + toktyp_t toktyp; + int lineno; + span_t str; + + // values represented by str + union { + const char *escp; // point to an esc char in str + int64_t int64; + double fp64; + bool b1; + struct { + // validity depends on toktyp for TIME, DATE, DATETIME, DATETIMETZ + int year, month, day, hour, minute, sec, usec; + int tz; // +- minutes + } tsval; + } u; +}; + +// Scanner object +struct scanner_t { + const char *src; // src[] is a NUL-terminated string + const char *endp; // end of src[]. always pointing at a NUL char. + const char *cur; // current char in src[] + int lineno; // line number of current char + char *errmsg; // set to ebuf.ptr if there was an error + ebuf_t ebuf; // buffer to store error message + + int bracket_level; // count depth of [ ] + int brace_level; // count depth of { } +}; +static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, + int errbufsz); +static int scan_key(scanner_t *sp, token_t *tok); +static int scan_value(scanner_t *sp, token_t *tok); +// restore scanner to state before tok was returned +static scanner_state_t scan_mark(scanner_t *sp); +static void scan_restore(scanner_t *sp, scanner_state_t state); + +#ifndef min +static inline int min(int a, int b) { return a < b ? a : b; } +#endif + +// Copy up to dstsz - 1 chars from the current position of the scanner +// to dst. +static void scan_copystr(scanner_t *sp, char *dst, int dstsz) { + assert(dstsz > 0); + int len = min(sp->endp - sp->cur, dstsz - 1); // account for NUL + if (len > 0) { + memcpy(dst, sp->cur, len); + dst[len] = 0; + } +} + +// Parser object +typedef struct parser_t parser_t; +struct parser_t { + scanner_t scanner; + toml_datum_t toptab; // top table + toml_datum_t *curtab; // current table + pool_t *pool; // memory pool for strings + ebuf_t ebuf; // buffer to store last error message +}; + +// Find key in tab and return its index. If not found, return -1. +static int tab_find(toml_datum_t *tab, span_t key) { + assert(tab->type == TOML_TABLE); + for (int i = 0, top = tab->u.tab.size; i < top; i++) { + if (tab->u.tab.len[i] == key.len && + 0 == memcmp(tab->u.tab.key[i], key.ptr, key.len)) { + return i; + } + } + return -1; +} + +// Put key into tab dictionary. Return a place to +// the datum for the key on success, or NULL otherwise. +static toml_datum_t *tab_emplace(toml_datum_t *tab, span_t key, + const char **reason) { + assert(tab->type == TOML_TABLE); + int i = tab_find(tab, key); + if (i >= 0) { + return &tab->u.tab.value[i]; + } + + // Expand pkey[], plen[] and value[]. The following does this + // separately for pkey, plen and value, and it is safe on partial + // failure, i.e. only the first one succeeded. + int N = tab->u.tab.size; + { + char **pkey = REALLOC(tab->u.tab.key, sizeof(*pkey) * align8(N + 1)); + if (!pkey) { + *reason = "out of memory"; + return NULL; + } + tab->u.tab.key = (const char **)pkey; + } + + { + int *plen = REALLOC(tab->u.tab.len, sizeof(*plen) * align8(N + 1)); + if (!plen) { + *reason = "out of memory"; + return NULL; + } + tab->u.tab.len = plen; + } + + { + toml_datum_t *value = + REALLOC(tab->u.tab.value, sizeof(*value) * align8(N + 1)); + if (!value) { + *reason = "out of memory"; + return NULL; + } + tab->u.tab.value = value; + } + + // Append the new key. The value is set to DATUM_ZERO. Caller will + // overwrite with a valid datum. + tab->u.tab.size = N + 1; + tab->u.tab.key[N] = (char *)key.ptr; + tab->u.tab.len[N] = key.len; + tab->u.tab.value[N] = DATUM_ZERO; + return &tab->u.tab.value[N]; +} + +// Add a new key in tab. Return 0 on success, -1 otherwise. +// On error, *reason will point to an error message. +static int tab_add(toml_datum_t *tab, span_t newkey, toml_datum_t newvalue, + const char **reason) { + assert(tab->type == TOML_TABLE); + toml_datum_t *pvalue = tab_emplace(tab, newkey, reason); + if (!pvalue) { + return -1; + } + if (pvalue->type) { + *reason = "duplicate key"; + return -1; + } + *pvalue = newvalue; + return 0; +} + +// Add a new element into an array. Return 0 on success, -1 otherwise. +// On error, *reason will point to an error message. +static toml_datum_t *arr_emplace(toml_datum_t *arr, const char **reason) { + assert(arr->type == TOML_ARRAY); + int n = arr->u.arr.size; + toml_datum_t *elem = REALLOC(arr->u.arr.elem, sizeof(*elem) * align8(n + 1)); + if (!elem) { + *reason = "out of memory"; + return NULL; + } + arr->u.arr.elem = elem; + arr->u.arr.size = n + 1; + elem[n] = DATUM_ZERO; + return &elem[n]; +} + +// ------------------- parser section +static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span); +static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret); +static int parse_keyvalue_expr(parser_t *pp, token_t tok); +static int parse_std_table_expr(parser_t *pp, token_t tok); +static int parse_array_table_expr(parser_t *pp, token_t tok); + +static toml_datum_t mkdatum(toml_type_t ty) { + toml_datum_t ret = {0}; + ret.type = ty; + if (ty == TOML_DATE || ty == TOML_TIME || ty == TOML_DATETIME || + ty == TOML_DATETIMETZ) { + ret.u.ts.year = -1; + ret.u.ts.month = -1; + ret.u.ts.day = -1; + ret.u.ts.hour = -1; + ret.u.ts.minute = -1; + ret.u.ts.second = -1; + ret.u.ts.usec = -1; + ret.u.ts.tz = -1; + } + return ret; +} + +// Recursively free any dynamically allocated memory in the datum tree +static void datum_free(toml_datum_t *datum) { + if (datum->type == TOML_TABLE) { + for (int i = 0, top = datum->u.tab.size; i < top; i++) { + datum_free(&datum->u.tab.value[i]); + } + FREE(datum->u.tab.key); + FREE(datum->u.tab.len); + FREE(datum->u.tab.value); + } else if (datum->type == TOML_ARRAY) { + for (int i = 0, top = datum->u.arr.size; i < top; i++) { + datum_free(&datum->u.arr.elem[i]); + } + FREE(datum->u.arr.elem); + } + // other types do not allocate memory + *datum = DATUM_ZERO; +} + +// Make a deep copy of src to dst. +// Return 0 on success, -1 otherwise. +static int datum_copy(toml_datum_t *dst, toml_datum_t src, pool_t *pool, + const char **reason) { + *dst = mkdatum(src.type); + switch (src.type) { + case TOML_STRING: + dst->u.str.ptr = pool_alloc(pool, src.u.str.len + 1); + if (!dst->u.str.ptr) { + *reason = "out of memory"; + goto bail; + } + dst->u.str.len = src.u.str.len; + memcpy((char *)dst->u.str.ptr, src.u.str.ptr, src.u.str.len + 1); + break; + case TOML_TABLE: + for (int i = 0; i < src.u.tab.size; i++) { + span_t newkey = {src.u.tab.key[i], src.u.tab.len[i]}; + toml_datum_t *pvalue = tab_emplace(dst, newkey, reason); + if (!pvalue) { + goto bail; + } + if (datum_copy(pvalue, src.u.tab.value[i], pool, reason)) { + goto bail; + } + } + break; + case TOML_ARRAY: + for (int i = 0; i < src.u.arr.size; i++) { + toml_datum_t *pelem = arr_emplace(dst, reason); + if (!pelem) { + goto bail; + } + if (datum_copy(pelem, src.u.arr.elem[i], pool, reason)) { + goto bail; + } + } + break; + default: + *dst = src; + break; + } + + return 0; + +bail: + datum_free(dst); + return -1; +} + +// Check if datum is an array of tables. +static inline bool is_array_of_tables(toml_datum_t datum) { + bool ret = (datum.type == TOML_ARRAY); + for (int i = 0; ret && i < datum.u.arr.size; i++) { + ret = (datum.u.arr.elem[i].type == TOML_TABLE); + } + return ret; +} + +// Merge src into dst. Return 0 on success, -1 otherwise. +static int datum_merge(toml_datum_t *dst, toml_datum_t src, pool_t *pool, + const char **reason) { + if (dst->type != src.type) { + datum_free(dst); + return datum_copy(dst, src, pool, reason); + } + switch (src.type) { + case TOML_TABLE: + // for key-value in src: + // override key-value in dst. + for (int i = 0; i < src.u.tab.size; i++) { + span_t key; + key.ptr = src.u.tab.key[i]; + key.len = src.u.tab.len[i]; + toml_datum_t *pvalue = tab_emplace(dst, key, reason); + if (!pvalue) { + return -1; + } + if (pvalue->type) { + DO(datum_merge(pvalue, src.u.tab.value[i], pool, reason)); + } else { + datum_free(pvalue); + DO(datum_copy(pvalue, src.u.tab.value[i], pool, reason)); + } + } + return 0; + case TOML_ARRAY: + if (is_array_of_tables(src)) { + // append src array to dst + for (int i = 0; i < src.u.arr.size; i++) { + toml_datum_t *pelem = arr_emplace(dst, reason); + if (!pelem) { + return -1; + } + DO(datum_copy(pelem, src.u.arr.elem[i], pool, reason)); + } + return 0; + } + // fallthru + default: + break; + } + datum_free(dst); + return datum_copy(dst, src, pool, reason); +} + +// Compare the content of a and b. +static bool datum_equiv(toml_datum_t a, toml_datum_t b) { + if (a.type != b.type) { + return false; + } + int N; + switch (a.type) { + case TOML_STRING: + return a.u.str.len == b.u.str.len && + 0 == memcmp(a.u.str.ptr, b.u.str.ptr, a.u.str.len); + case TOML_INT64: + return a.u.int64 == b.u.int64; + case TOML_FP64: + return a.u.fp64 == b.u.fp64 || (isnan(a.u.fp64) && isnan(b.u.fp64)); + case TOML_BOOLEAN: + return !!a.u.boolean == !!b.u.boolean; + case TOML_DATE: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day; + case TOML_TIME: + return a.u.ts.hour == b.u.ts.hour && a.u.ts.minute == b.u.ts.minute && + a.u.ts.second == b.u.ts.second && a.u.ts.usec == b.u.ts.usec; + case TOML_DATETIME: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && + a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && + a.u.ts.usec == b.u.ts.usec; + case TOML_DATETIMETZ: + return a.u.ts.year == b.u.ts.year && a.u.ts.month == b.u.ts.month && + a.u.ts.day == b.u.ts.day && a.u.ts.hour == b.u.ts.hour && + a.u.ts.minute == b.u.ts.minute && a.u.ts.second == b.u.ts.second && + a.u.ts.usec == b.u.ts.usec && a.u.ts.tz == b.u.ts.tz; + case TOML_ARRAY: + N = a.u.arr.size; + if (N != b.u.arr.size) { + return false; + } + for (int i = 0; i < N; i++) { + if (!datum_equiv(a.u.arr.elem[i], b.u.arr.elem[i])) { + return false; + } + } + return true; + case TOML_TABLE: + N = a.u.tab.size; + if (N != b.u.tab.size) { + return false; + } + for (int i = 0; i < N; i++) { + int len = a.u.tab.len[i]; + if (len != b.u.tab.len[i]) { + return false; + } + if (0 != memcmp(a.u.tab.key[i], b.u.tab.key[i], len)) { + return false; + } + if (!datum_equiv(a.u.tab.value[i], b.u.tab.value[i])) { + return false; + } + } + return true; + default: + break; + } + return false; +} + +/** + * Override values in r1 using r2. Return a new result. All results + * (i.e., r1, r2 and the returned result) must be freed using toml_free() + * after use. + * + * LOGIC: + * ret = copy of r1 + * for each item x in r2: + * if x is not in ret: + * override + * elif x in ret is NOT of the same type: + * override + * elif x is an array of tables: + * append r2.x to ret.x + * elif x is a table: + * merge r2.x to ret.x + * else: + * override + */ +toml_result_t toml_merge(const toml_result_t *r1, const toml_result_t *r2) { + const char *reason = ""; + toml_result_t ret = {0}; + pool_t *pool = 0; + if (!r1->ok) { + reason = "param error: r1 not ok"; + goto bail; + } + if (!r2->ok) { + reason = "param error: r2 not ok"; + goto bail; + } + { + pool_t *r1pool = (pool_t *)r1->__internal; + pool_t *r2pool = (pool_t *)r2->__internal; + pool = pool_create(r1pool->top + r2pool->top); + if (!pool) { + reason = "out of memory"; + goto bail; + } + } + + // Make a copy of r1 + if (datum_copy(&ret.toptab, r1->toptab, pool, &reason)) { + goto bail; + } + + // Merge r2 into the result + if (datum_merge(&ret.toptab, r2->toptab, pool, &reason)) { + goto bail; + } + + ret.ok = 1; + ret.__internal = pool; + return ret; + +bail: + pool_destroy(pool); + snprintf(ret.errmsg, sizeof(ret.errmsg), "%s", reason); + return ret; +} + +bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2) { + if (!(r1->ok && r2->ok)) { + return false; + } + return datum_equiv(r1->toptab, r2->toptab); +} + +/** + * Find a key in a toml_table. Return the value of the key if found, + * or a TOML_UNKNOWN otherwise. + */ +toml_datum_t toml_get(toml_datum_t datum, const char *key) { + toml_datum_t ret = {0}; + if (datum.type == TOML_TABLE) { + int n = datum.u.tab.size; + const char **pkey = datum.u.tab.key; + toml_datum_t *pvalue = datum.u.tab.value; + for (int i = 0; i < n; i++) { + if (0 == strcmp(pkey[i], key)) { + return pvalue[i]; + } + } + } + return ret; +} + +/** + * Locate a value starting from a toml_table. Return the value of the key if + * found, or a TOML_UNKNOWN otherwise. + * + * Note: the multipart-key is separated by DOT, and must not have any escape + * chars. + */ +toml_datum_t toml_seek(toml_datum_t table, const char *multipart_key) { + if (table.type != TOML_TABLE) { + return DATUM_ZERO; + } + + // Make a mutable copy of the multipart_key for splitting + char buf[256]; + if (copystring(buf, sizeof(buf), multipart_key)) { + return DATUM_ZERO; + } + + // Go through the multipart name part by part. + char *p = buf; // start of current key + char *q = strchr(p, '.'); // end of current key + toml_datum_t datum = table; + while (q && datum.type == TOML_TABLE) { + *q = 0; + datum = toml_get(datum, p); + if (datum.type == TOML_TABLE) { + p = q + 1; + q = strchr(p, '.'); + } + } + + if (!q && datum.type == TOML_TABLE) { + return toml_get(datum, p); + } + + return DATUM_ZERO; +} + +/** + * Return the default options. + */ +toml_option_t toml_default_option(void) { + toml_option_t opt = {0, realloc, free}; + return opt; +} + +/** + * Override the current options. + */ +void toml_set_option(toml_option_t opt) { toml_option = opt; } + +/** + * Free the result returned by toml_parse(). + */ +void toml_free(toml_result_t result) { + datum_free(&result.toptab); + pool_destroy((pool_t *)result.__internal); +} + +/** + * Parse a toml document. + */ +toml_result_t toml_parse_file_ex(const char *fname) { + toml_result_t result = {0}; + FILE *fp = fopen(fname, "r"); + if (!fp) { + snprintf(result.errmsg, sizeof(result.errmsg), "fopen: %s", fname); + return result; + } + result = toml_parse_file(fp); + fclose(fp); + return result; +} + +/** + * Parse a toml document. + */ +toml_result_t toml_parse_file(FILE *fp) { + toml_result_t result = {0}; + char *buf = 0; + int top, max; // index into buf[] + top = max = 0; + + // Read file into memory + while (!feof(fp)) { + assert(top <= max); + if (top == max) { + // need to extend buf[] + int64_t tmpmax64 = (int64_t)max * 3 / 2 + 1000; + int tmpmax = (tmpmax64 > INT_MAX - 1) ? INT_MAX - 1 : (int)tmpmax64; + if (tmpmax == INT_MAX - 1) { + snprintf(result.errmsg, sizeof(result.errmsg), "file is too big"); + FREE(buf); + return result; + } + // add an extra byte for terminating NUL + char *tmp = REALLOC(buf, tmpmax + 1); + if (!tmp) { + snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); + FREE(buf); + return result; + } + buf = tmp; + max = tmpmax; + } + + errno = 0; + top += fread(buf + top, 1, max - top, fp); + if (ferror(fp)) { + snprintf(result.errmsg, sizeof(result.errmsg), "%s", + errno ? strerror(errno) : "Error reading file"); + FREE(buf); + return result; + } + } + buf[top] = 0; // NUL terminator + + result = toml_parse(buf, top); + FREE(buf); + return result; +} + +/** + * Parse a toml document. + */ +toml_result_t toml_parse(const char *src, int len) { + toml_result_t result = {0}; + parser_t parser = {0}; + parser_t *pp = &parser; + + // Check that src is NUL terminated. + if (src[len]) { + snprintf(result.errmsg, sizeof(result.errmsg), + "src[] must be NUL terminated"); + goto bail; + } + + // If user insists, check that src[] is a valid utf8 string. + if (toml_option.check_utf8) { + int line = 1; // keeps track of line number + for (int i = 0; i < len;) { + uint32_t ch; + int n = utf8_to_ucs(src + i, len - i, &ch); + if (n < 0) { + snprintf(result.errmsg, sizeof(result.errmsg), + "invalid UTF8 char on line %d", line); + goto bail; + } + if (0xD800 <= ch && ch <= 0xDFFF) { + // explicitly prohibit surrogates (non-scalar unicode code point) + snprintf(result.errmsg, sizeof(result.errmsg), + "invalid UTF8 char \\u%04x on line %d", ch, line); + goto bail; + } + line += (ch == '\n' ? 1 : 0); + i += n; + } + } + + // Initialize parser + pp->toptab = mkdatum(TOML_TABLE); + pp->curtab = &pp->toptab; + pp->ebuf.ptr = result.errmsg; // parse error will be printed into pp->ebuf + pp->ebuf.len = sizeof(result.errmsg); + + // Alloc memory pool + pp->pool = + pool_create(len + 10); // add some extra bytes for NUL term and safety + if (!pp->pool) { + snprintf(result.errmsg, sizeof(result.errmsg), "out of memory"); + goto bail; + } + + // Initialize scanner. Scan error will be printed into pp->ebuf. + scan_init(&pp->scanner, src, len, pp->ebuf.ptr, pp->ebuf.len); + + // Keep parsing until FIN + for (;;) { + token_t tok; + if (scan_key(&pp->scanner, &tok)) { + goto bail; + } + // break on FIN + if (tok.toktyp == TOK_FIN) { + break; + } + switch (tok.toktyp) { + case TOK_ENDL: // skip blank lines + continue; + case TOK_LBRACK: + if (parse_std_table_expr(pp, tok)) { + goto bail; + } + break; + case TOK_LLBRACK: + if (parse_array_table_expr(pp, tok)) { + goto bail; + } + break; + default: + // non-blank line: parse an expression + if (parse_keyvalue_expr(pp, tok)) { + goto bail; + } + break; + } + // each expression must be followed by newline + if (scan_key(&pp->scanner, &tok)) { + goto bail; + } + if (tok.toktyp == TOK_FIN || tok.toktyp == TOK_ENDL) { + continue; + } + SETERROR(pp->ebuf, tok.lineno, "ENDL expected"); + goto bail; + } + + // return result + result.ok = true; + result.toptab = pp->toptab; + result.__internal = (void *)pp->pool; + return result; + +bail: + // return error + datum_free(&pp->toptab); + pool_destroy(pp->pool); + result.ok = false; + if (result.errmsg[0] == '\0') { + assert(0); + snprintf(result.errmsg, sizeof(result.errmsg), "Error parsing TOML file"); + } + return result; +} + +// Convert a (LITSTRING, LIT, MLLITSTRING, MLSTRING, or STRING) token to a +// datum. +static int token_to_string(parser_t *pp, token_t tok, toml_datum_t *ret) { + *ret = mkdatum(TOML_STRING); + span_t span; + DO(parse_norm(pp, tok, &span)); + ret->u.str.ptr = (char *)span.ptr; + ret->u.str.len = span.len; + return 0; +} + +// Convert a TIME/DATE/DATETIME/DATETIMETZ to a datum +static int token_to_timestamp(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + static const toml_type_t map[] = {[TOK_TIME] = TOML_TIME, + [TOK_DATE] = TOML_DATE, + [TOK_DATETIME] = TOML_DATETIME, + [TOK_DATETIMETZ] = TOML_DATETIMETZ}; + switch (tok.toktyp) { + case TOK_TIME: + case TOK_DATE: + case TOK_DATETIME: + case TOK_DATETIMETZ: + break; + default: + assert(0 && "unexpected token type"); + return -1; + } + + *ret = mkdatum(map[tok.toktyp]); + ret->u.ts.year = tok.u.tsval.year; + ret->u.ts.month = tok.u.tsval.month; + ret->u.ts.day = tok.u.tsval.day; + ret->u.ts.hour = tok.u.tsval.hour; + ret->u.ts.minute = tok.u.tsval.minute; + ret->u.ts.second = tok.u.tsval.sec; + ret->u.ts.usec = tok.u.tsval.usec; + ret->u.ts.tz = tok.u.tsval.tz; + return 0; +} + +// Convert an int64 token to a datum. +static int token_to_int64(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_INTEGER); + *ret = mkdatum(TOML_INT64); + ret->u.int64 = tok.u.int64; + return 0; +} + +// Convert a fp64 token to a datum. +static int token_to_fp64(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_FLOAT); + *ret = mkdatum(TOML_FP64); + ret->u.fp64 = tok.u.fp64; + return 0; +} + +// Convert a boolean token to a datum. +static int token_to_boolean(parser_t *pp, token_t tok, toml_datum_t *ret) { + (void)pp; + assert(tok.toktyp == TOK_BOOL); + *ret = mkdatum(TOML_BOOLEAN); + ret->u.boolean = tok.u.b1; + return 0; +} + +// Parse a multipart key. Return 0 on success, -1 otherwise. +static int parse_key(parser_t *pp, token_t tok, keypart_t *ret_keypart) { + ret_keypart->nspan = 0; + // key = simple-key | dotted_key + // simple-key = STRING | LITSTRING | LIT + // dotted-key = simple-key (DOT simple-key)+ + if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && + tok.toktyp != TOK_LIT) { + return SETERROR(pp->ebuf, tok.lineno, "missing key"); + } + + int n = 0; + span_t *kpspan = ret_keypart->span; + + // Normalize the first keypart + if (parse_norm(pp, tok, &kpspan[n])) { + return SETERROR(pp->ebuf, tok.lineno, + "unable to normalize string; probably a unicode issue"); + } + n++; + + // Scan and normalize the second to last keypart + while (1) { + scanner_state_t mark = scan_mark(&pp->scanner); + + // Eat the dot if it is there + DO(scan_key(&pp->scanner, &tok)); + + // If not a dot, we are done with keyparts. + if (tok.toktyp != TOK_DOT) { + scan_restore(&pp->scanner, mark); + break; + } + + // Scan the n-th key + DO(scan_key(&pp->scanner, &tok)); + + if (tok.toktyp != TOK_STRING && tok.toktyp != TOK_LITSTRING && + tok.toktyp != TOK_LIT) { + return SETERROR(pp->ebuf, tok.lineno, "expects a string in dotted-key"); + } + + if (n >= KEYPARTMAX) { + return SETERROR(pp->ebuf, tok.lineno, "too many key parts"); + } + + // Normalize the n-th key. + DO(parse_norm(pp, tok, &kpspan[n])); + n++; + } + + // This key has n parts. + ret_keypart->nspan = n; + return 0; +} + +// Starting at toptab, descend following keypart[]. If a key does not +// exist in the current table, create a new table entry for the +// key. Returns the final table represented by the key. +static toml_datum_t *descend_keypart(parser_t *pp, int lineno, + toml_datum_t *toptab, keypart_t *keypart, + bool stdtabexpr) { + toml_datum_t *tab = toptab; // current tab + + for (int i = 0; i < keypart->nspan; i++) { + const char *reason; + // Find the i-th keypart + int j = tab_find(tab, keypart->span[i]); + // Not found: add a new (key, tab) pair. + if (j < 0) { + toml_datum_t newtab = mkdatum(TOML_TABLE); + newtab.flag |= stdtabexpr ? FLAG_STDEXPR : 0; + if (tab_add(tab, keypart->span[i], newtab, &reason)) { + SETERROR(pp->ebuf, lineno, "%s", reason); + return NULL; + } + tab = &tab->u.tab.value[tab->u.tab.size - 1]; // descend + continue; + } + + // Found: extract the value of the key. + toml_datum_t *value = &tab->u.tab.value[j]; + + // If the value is a table, descend. + if (value->type == TOML_TABLE) { + tab = value; // descend + continue; + } + + // If the value is an array: locate the last entry and descend. + if (value->type == TOML_ARRAY) { + // If empty: error. + if (value->u.arr.size <= 0) { + SETERROR(pp->ebuf, lineno, "array %s has no elements", + keypart->span[i].ptr); + return NULL; + } + + // Extract the last element of the array. + value = &value->u.arr.elem[value->u.arr.size - 1]; + + // It must be a table! + if (value->type != TOML_TABLE) { + SETERROR(pp->ebuf, lineno, "array %s must be array of tables", + keypart->span[i].ptr); + return NULL; + } + tab = value; // descend + continue; + } + + // key not found + SETERROR(pp->ebuf, lineno, "cannot locate table at key %s", + keypart->span[i].ptr); + return NULL; + } + + // Return the table corresponding to the keypart[]. + return tab; +} + +// Recursively set flags on datum +static void set_flag_recursive(toml_datum_t *datum, uint32_t flag) { + datum->flag |= flag; + switch (datum->type) { + case TOML_ARRAY: + for (int i = 0, top = datum->u.arr.size; i < top; i++) { + set_flag_recursive(&datum->u.arr.elem[i], flag); + } + break; + case TOML_TABLE: + for (int i = 0, top = datum->u.tab.size; i < top; i++) { + set_flag_recursive(&datum->u.tab.value[i], flag); + } + break; + default: + break; + } +} + +// Parse an inline array. +static int parse_inline_array(parser_t *pp, token_t tok, + toml_datum_t *ret_datum) { + assert(tok.toktyp == TOK_LBRACK); + *ret_datum = mkdatum(TOML_ARRAY); + int need_comma = 0; + + // loop until RBRACK + for (;;) { + // skip ENDL + do { + DO(scan_value(&pp->scanner, &tok)); + } while (tok.toktyp == TOK_ENDL); + + // If got an RBRACK: done! + if (tok.toktyp == TOK_RBRACK) { + break; + } + + // If got a COMMA: check if it is expected. + if (tok.toktyp == TOK_COMMA) { + if (need_comma) { + need_comma = 0; + continue; + } + return SETERROR(pp->ebuf, tok.lineno, + "syntax error while parsing array: unexpected comma"); + } + + // Not a comma, but need a comma: error! + if (need_comma) { + return SETERROR(pp->ebuf, tok.lineno, + "syntax error while parsing array: missing comma"); + } + + // This is a valid value! + + // Add the value to the array. + const char *reason; + toml_datum_t *pelem = arr_emplace(ret_datum, &reason); + if (!pelem) { + return SETERROR(pp->ebuf, tok.lineno, "while parsing array: %s", reason); + } + + // Parse the value and save into array. + DO(parse_val(pp, tok, pelem)); + + // Need comma before the next value. + need_comma = 1; + } + + // Set the INLINE flag for all things in this array. + set_flag_recursive(ret_datum, FLAG_INLINED); + return 0; +} + +// Parse an inline table. +static int parse_inline_table(parser_t *pp, token_t tok, + toml_datum_t *ret_datum) { + assert(tok.toktyp == TOK_LBRACE); + *ret_datum = mkdatum(TOML_TABLE); + bool need_comma = 0; + bool was_comma = 0; + + // loop until RBRACE + for (;;) { + DO(scan_key(&pp->scanner, &tok)); + + // Got an RBRACE: done! + if (tok.toktyp == TOK_RBRACE) { + if (was_comma) { + /* + return SETERROR(pp->ebuf, tok.lineno, + "extra comma before closing brace"); + */ + // extra comma before RBRACE is allowed for v1.1 + (void)0; + } + break; + } + + // Got a comma: check if it is expected. + if (tok.toktyp == TOK_COMMA) { + if (need_comma) { + need_comma = 0, was_comma = 1; + continue; + } + return SETERROR(pp->ebuf, tok.lineno, "unexpected comma"); + } + + // Newline not allowed in inline table. + // newline is allowed in v1.1 + if (tok.toktyp == TOK_ENDL) { + // return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); + continue; + } + + // Not a comma, but need a comma: error! + if (need_comma) { + return SETERROR(pp->ebuf, tok.lineno, "missing comma"); + } + + // Get the keyparts + keypart_t keypart = {0}; + int keylineno = tok.lineno; + DO(parse_key(pp, tok, &keypart)); + + // Descend to one keypart before last + span_t lastkeypart = keypart.span[--keypart.nspan]; + toml_datum_t *tab = + descend_keypart(pp, keylineno, ret_datum, &keypart, false); + if (!tab) { + return -1; + } + + // If tab is a previously declared inline table: error. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, tok.lineno, "inline table cannot be extended"); + } + + // We are explicitly defining it now. + tab->flag |= FLAG_EXPLICIT; + + // match EQUAL + DO(scan_value(&pp->scanner, &tok)); + + if (tok.toktyp != TOK_EQUAL) { + if (tok.toktyp == TOK_ENDL) { + return SETERROR(pp->ebuf, tok.lineno, "unexpected newline"); + } else { + return SETERROR(pp->ebuf, tok.lineno, "missing '='"); + } + } + + // obtain the value + toml_datum_t value; + DO(scan_value(&pp->scanner, &tok)); + DO(parse_val(pp, tok, &value)); + + // Add the value to tab. + const char *reason; + if (tab_add(tab, lastkeypart, value, &reason)) { + return SETERROR(pp->ebuf, tok.lineno, "%s", reason); + } + need_comma = 1, was_comma = 0; + } + + set_flag_recursive(ret_datum, FLAG_INLINED); + return 0; +} + +// Parse a value. +static int parse_val(parser_t *pp, token_t tok, toml_datum_t *ret) { + // val = string / boolean / array / inline-table / date-time / float / integer + switch (tok.toktyp) { + case TOK_STRING: + case TOK_MLSTRING: + case TOK_LITSTRING: + case TOK_MLLITSTRING: + return token_to_string(pp, tok, ret); + case TOK_TIME: + case TOK_DATE: + case TOK_DATETIME: + case TOK_DATETIMETZ: + return token_to_timestamp(pp, tok, ret); + case TOK_INTEGER: + return token_to_int64(pp, tok, ret); + case TOK_FLOAT: + return token_to_fp64(pp, tok, ret); + case TOK_BOOL: + return token_to_boolean(pp, tok, ret); + case TOK_LBRACK: // inline-array + return parse_inline_array(pp, tok, ret); + case TOK_LBRACE: // inline-table + return parse_inline_table(pp, tok, ret); + default: + break; + } + return SETERROR(pp->ebuf, tok.lineno, "missing value"); +} + +// Parse a standard table expression, and set the curtab of the parser +// to the table referenced. A standard table expression is a line +// like [a.b.c.d]. +static int parse_std_table_expr(parser_t *pp, token_t tok) { + // std-table = [ key ] + // Eat the [ + assert(tok.toktyp == TOK_LBRACK); // [ ate by caller + + // Read the first keypart + DO(scan_key(&pp->scanner, &tok)); + + // Extract the keypart[] + int keylineno = tok.lineno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // Eat the ] + DO(scan_key(&pp->scanner, &tok)); + if (tok.toktyp != TOK_RBRACK) { + return SETERROR(pp->ebuf, tok.lineno, "missing right-bracket"); + } + + // Descend to one keypart before last. + span_t lastkeypart = keypart.span[--keypart.nspan]; + + // Descend keypart from the toptab. + toml_datum_t *tab = + descend_keypart(pp, keylineno, &pp->toptab, &keypart, true); + if (!tab) { + return -1; + } + + // Look for the last keypart in the final tab + int j = tab_find(tab, lastkeypart); + if (j < 0) { + // If not found: add it. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); + } + const char *reason; + toml_datum_t newtab = mkdatum(TOML_TABLE); + newtab.flag |= FLAG_STDEXPR; + if (tab_add(tab, lastkeypart, newtab, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + // this is the new tab + tab = &tab->u.tab.value[tab->u.tab.size - 1]; + } else { + // Found: check for errors + tab = &tab->u.tab.value[j]; + if (tab->flag & FLAG_EXPLICIT) { + /* + This is not OK: + [x.y.z] + [x.y.z] + + but this is OK: + [x.y.z] + [x] + */ + return SETERROR(pp->ebuf, keylineno, "table defined more than once"); + } + if (!(tab->flag & FLAG_STDEXPR)) { + /* + [t1] # OK + t2.t3.v = 0 # OK + [t1.t2] # should FAIL - t2 was non-explicit but was not + created by std-table-expr + */ + return SETERROR(pp->ebuf, keylineno, "table defined before"); + } + } + + // Set explicit flag on tab + tab->flag |= FLAG_EXPLICIT; + + // Set tab as curtab of the parser + pp->curtab = tab; + return 0; +} + +// Parse an array table expression, and set the curtab of the parser +// to the table referenced. A standard array table expresison is a line +// like [[a.b.c.d]]. +static int parse_array_table_expr(parser_t *pp, token_t tok) { + // array-table = [[ key ]] + assert(tok.toktyp == TOK_LLBRACK); // [[ ate by caller + + // Read the first keypart + DO(scan_key(&pp->scanner, &tok)); + + int keylineno = tok.lineno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // eat the ]] + token_t rrb; + DO(scan_key(&pp->scanner, &rrb)); + if (rrb.toktyp != TOK_RRBRACK) { + return SETERROR(pp->ebuf, rrb.lineno, "missing ']]'"); + } + + // remove the last keypart from keypart[] + span_t lastkeypart = keypart.span[--keypart.nspan]; + + // descend the key from the toptab + toml_datum_t *tab = &pp->toptab; + for (int i = 0; i < keypart.nspan; i++) { + span_t curkey = keypart.span[i]; + int j = tab_find(tab, curkey); + if (j < 0) { + // If not found: add a new (key,tab) pair + const char *reason; + toml_datum_t newtab = mkdatum(TOML_TABLE); + newtab.flag |= FLAG_STDEXPR; + if (tab_add(tab, curkey, newtab, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + tab = &tab->u.tab.value[tab->u.tab.size - 1]; + continue; + } + + // Found: get the value + toml_datum_t *value = &tab->u.tab.value[j]; + + // If value is table, then point to that table and continue descent. + if (value->type == TOML_TABLE) { + tab = value; + continue; + } + + // If value is an array of table, point to the last element of the array and + // continue descent. + if (value->type == TOML_ARRAY) { + if (value->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "cannot expand array %s", + curkey.ptr); + } + if (value->u.arr.size <= 0) { + return SETERROR(pp->ebuf, keylineno, "array %s has no elements", + curkey.ptr); + } + value = &value->u.arr.elem[value->u.arr.size - 1]; + if (value->type != TOML_TABLE) { + return SETERROR(pp->ebuf, keylineno, "array %s must be array of tables", + curkey.ptr); + } + tab = value; + continue; + } + + // keypart not found + return SETERROR(pp->ebuf, keylineno, "cannot locate table at key %s", + curkey.ptr); + } + + // For the final keypart, make sure entry at key is an array of tables + const char *reason; + int idx = tab_find(tab, lastkeypart); + if (idx == -1) { + // If not found, add an array of table. + if (tab_add(tab, lastkeypart, mkdatum(TOML_ARRAY), &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + idx = tab_find(tab, lastkeypart); + assert(idx >= 0); + } + // Check that this is an array. + if (tab->u.tab.value[idx].type != TOML_ARRAY) { + return SETERROR(pp->ebuf, keylineno, "entry must be an array"); + } + // Add an empty table to the array + toml_datum_t *arr = &tab->u.tab.value[idx]; + if (arr->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "cannot extend a static array"); + } + toml_datum_t *pelem = arr_emplace(arr, &reason); + if (!pelem) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + *pelem = mkdatum(TOML_TABLE); + + // Set the last element of this array as curtab of the parser + pp->curtab = &arr->u.arr.elem[arr->u.arr.size - 1]; + assert(pp->curtab->type == TOML_TABLE); + + return 0; +} + +// Parse an expression. A toml doc is just a list of expressions. +static int parse_keyvalue_expr(parser_t *pp, token_t tok) { + // Obtain the key + int keylineno = tok.lineno; + keypart_t keypart; + DO(parse_key(pp, tok, &keypart)); + + // match the '=' + DO(scan_key(&pp->scanner, &tok)); + if (tok.toktyp != TOK_EQUAL) { + return SETERROR(pp->ebuf, tok.lineno, "expect '='"); + } + + // Obtain the value + toml_datum_t val; + DO(scan_value(&pp->scanner, &tok)); + DO(parse_val(pp, tok, &val)); + + // Locate the last table using keypart[] + const char *reason; + toml_datum_t *tab = pp->curtab; + for (int i = 0; i < keypart.nspan - 1; i++) { + int j = tab_find(tab, keypart.span[i]); + if (j < 0) { + if (i > 0 && (tab->flag & FLAG_EXPLICIT)) { + return SETERROR( + pp->ebuf, keylineno, + "cannot extend a previously defined table using dotted expression"); + } + toml_datum_t newtab = mkdatum(TOML_TABLE); + if (tab_add(tab, keypart.span[i], newtab, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + tab = &tab->u.tab.value[tab->u.tab.size - 1]; + continue; + } + toml_datum_t *value = &tab->u.tab.value[j]; + if (value->type == TOML_TABLE) { + tab = value; + continue; + } + if (value->type == TOML_ARRAY) { + return SETERROR(pp->ebuf, keylineno, + "encountered previously declared array '%s'", + keypart.span[i].ptr); + } + return SETERROR(pp->ebuf, keylineno, "cannot locate table at '%s'", + keypart.span[i].ptr); + } + + // Check for disallowed situations. + if (tab->flag & FLAG_INLINED) { + return SETERROR(pp->ebuf, keylineno, "inline table cannot be extended"); + } + if (keypart.nspan > 1 && (tab->flag & FLAG_EXPLICIT)) { + return SETERROR( + pp->ebuf, keylineno, + "cannot extend a previously defined table using dotted expression"); + } + + // Add a new key/value for tab. + if (tab_add(tab, keypart.span[keypart.nspan - 1], val, &reason)) { + return SETERROR(pp->ebuf, keylineno, "%s", reason); + } + + return 0; +} + +// Normalize a LIT/STRING/MLSTRING/LITSTRING/MLLITSTRING +// -> unescape all escaped chars +// The returned string is allocated out of pp->sbuf[] +static int parse_norm(parser_t *pp, token_t tok, span_t *ret_span) { + // Allocate a buffer to store the normalized string. Add one + // extra-byte for terminating NUL. + char *p = pool_alloc(pp->pool, tok.str.len + 1); + if (!p) { + return SETERROR(pp->ebuf, tok.lineno, "out of memory"); + } + + // Copy from token string into buffer + memcpy(p, tok.str.ptr, tok.str.len); + p[tok.str.len] = 0; // additional NUL term for safety + + ret_span->ptr = p; + ret_span->len = tok.str.len; + + switch (tok.toktyp) { + case TOK_LIT: + case TOK_LITSTRING: + case TOK_MLLITSTRING: + // no need to handle escape chars + return 0; + + case TOK_STRING: + case TOK_MLSTRING: + // need to handle escape chars + break; + + default: + return SETERROR(pp->ebuf, 0, "internal: arg must be a string"); + } + + // if there is no escape char, then done! + if (!tok.u.escp) { + return 0; // success + } + + // p points to the backslash + p += (tok.u.escp - tok.str.ptr); + assert(p - ret_span->ptr == tok.u.escp - tok.str.ptr); + assert(*p == '\\'); + + // Normalize the escaped chars + char *dst = p; + while (*p) { + if (*p != '\\') { + *dst++ = *p++; + continue; + } + switch (p[1]) { + case '"': + case '\\': + *dst++ = p[1]; + p += 2; + continue; + case 'b': + *dst++ = '\b'; + p += 2; + continue; + case 't': + *dst++ = '\t'; + p += 2; + continue; + case 'n': + *dst++ = '\n'; + p += 2; + continue; + case 'f': + *dst++ = '\f'; + p += 2; + continue; + case 'r': + *dst++ = '\r'; + p += 2; + continue; + case 'e': + *dst++ = '\e'; + p += 2; + continue; + case 'x': { + char buf[3]; + memcpy(buf, p + 2, 2); + buf[2] = 0; + int32_t ucs = strtol(buf, 0, 16); + int n = ucs_to_utf8(ucs, dst); + if (n < 0) { + return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", + buf); + } + dst += n; + p += 2 + 2; // \xNN + continue; + } + case 'u': + case 'U': { + char buf[9]; + int sz = (p[1] == 'u' ? 4 : 8); + memcpy(buf, p + 2, sz); + buf[sz] = 0; + int32_t ucs = strtol(buf, 0, 16); + if (0xD800 <= ucs && ucs <= 0xDFFF) { + // explicitly prohibit surrogates (non-scalar unicode code point) + return SETERROR(pp->ebuf, tok.lineno, "invalid UTF8 char \\u%04x", ucs); + } + int n = ucs_to_utf8(ucs, dst); + if (n < 0) { + return SETERROR(pp->ebuf, tok.lineno, "error converting UCS %s to UTF8", + buf); + } + dst += n; + p += 2 + sz; // \uNNNN or \UNNNNNNNN + continue; + } + + case ' ': + case '\t': + case '\r': + // line-ending backslash + // --- allow for extra whitespace chars after backslash + // --- skip until newline + p++; // skip the escape char + p += strspn(p, " \t\r"); // skip whitespaces + if (*p != '\n') { + return SETERROR(pp->ebuf, tok.lineno, + "unexpected char after line-ending backslash"); + } + // fallthru + case '\n': + // skip all whitespaces including newline + p++; + p += strspn(p, " \t\r\n"); + continue; + default: + *dst++ = *p++; + continue; + } + } + *dst = 0; + ret_span->len = dst - ret_span->ptr; + return 0; +} + +// =================================================================== +// == SCANNER SECTOIN +// =================================================================== + +// Get the next char +static int scan_get(scanner_t *sp) { + int ret = TOK_FIN; + const char *p = sp->cur; + if (p < sp->endp) { + ret = *p++; + if (ret == '\r' && p < sp->endp && *p == '\n') { + ret = *p++; + } + } + sp->cur = p; + sp->lineno += (ret == '\n' ? 1 : 0); + return ret; +} + +// Check if the next char matches ch. +static inline bool scan_match(scanner_t *sp, int ch) { + const char *p = sp->cur; + // exact match? done. + if (p < sp->endp && *p == ch) { + return true; + } + // \n also matches \r\n + if (ch == '\n' && p + 1 < sp->endp) { + return p[0] == '\r' && p[1] == '\n'; + } + // not a match + return false; +} + +// Check if the next char is in accept[]. +static bool scan_matchany(scanner_t *sp, const char *accept) { + for (; *accept; accept++) { + if (scan_match(sp, *accept)) { + return true; + } + } + return false; +} + +// Check if the next n chars match ch. +static inline bool scan_nmatch(scanner_t *sp, int ch, int n) { + assert(ch != '\n'); // not handled + if (sp->cur + n > sp->endp) { + return false; + } + const char *p = sp->cur; + int i; + for (i = 0; i < n && p[i] == ch; i++) + ; + return i == n; +} + +// Initialize a token. +static inline token_t mktoken(scanner_t *sp, toktyp_t typ) { + token_t tok = {0}; + tok.toktyp = typ; + tok.str.ptr = sp->cur; + tok.lineno = sp->lineno; + return tok; +} + +#define S_GET() scan_get(sp) +#define S_MATCH(ch) scan_match(sp, (ch)) +#define S_MATCH3(ch) scan_nmatch(sp, (ch), 3) +#define S_MATCH4(ch) scan_nmatch(sp, (ch), 4) +#define S_MATCH6(ch) scan_nmatch(sp, (ch), 6) + +static inline bool is_valid_char(int ch) { + // i.e. (0x20 <= ch && ch <= 0x7e) || (ch & 0x80); + return isprint(ch) || (ch & 0x80); +} + +static inline bool is_hex_char(int ch) { + ch = toupper(ch); + return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F'); +} + +// Initialize a scanner +static void scan_init(scanner_t *sp, const char *src, int len, char *errbuf, + int errbufsz) { + memset(sp, 0, sizeof(*sp)); + sp->src = src; + sp->endp = src + len; + assert(*sp->endp == '\0'); + sp->cur = src; + sp->lineno = 1; + sp->ebuf.ptr = errbuf; + sp->ebuf.len = errbufsz; +} + +static int scan_multiline_string(scanner_t *sp, token_t *tok) { + assert(S_MATCH3('"')); + S_GET(), S_GET(), S_GET(); // skip opening """ + + // According to spec: trim first newline after """ + if (S_MATCH('\n')) { + S_GET(); + } + + *tok = mktoken(sp, TOK_MLSTRING); + // scan until terminating """ + const char *escp = NULL; + while (1) { + if (S_MATCH3('"')) { + if (S_MATCH4('"')) { + // special case... """abcd """" -> (abcd ") + // but sequences of 3 or more double quotes are not allowed + if (S_MATCH6('"')) { + return SETERROR(sp->ebuf, sp->lineno, + "detected sequences of 3 or more double quotes"); + } else { + ; // no problem + } + } else { + break; // found terminating """ + } + } + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated \"\"\""); + } + // If non-escaped char ... + if (ch != '\\') { + if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + continue; + } + // ch is backslash + if (!escp) { + escp = sp->cur - 1; + assert(*escp == '\\'); + } + + // handle escape char + ch = S_GET(); + if (ch && strchr("btnfre\"\\", ch)) { + // skip \", \\, \b, \f, \n, \r, \t + continue; + } + int top = 0; + switch (ch) { + case 'x': + top = 2; + break; + case 'u': + top = 4; + break; + case 'U': + top = 8; + break; + default: + break; + } + if (top) { + for (int i = 0; i < top; i++) { + if (!is_hex_char(S_GET())) { + return SETERROR(sp->ebuf, sp->lineno, + "expect %d hex digits after \\%c", top, ch); + } + } + continue; + } + // handle line-ending backslash + if (ch == ' ' || ch == '\t') { + // Although the spec does not allow for whitespace following a + // line-ending backslash, some standard tests expect it. + // Skip whitespace till EOL. + while (ch != TOK_FIN && ch && strchr(" \t", ch)) { + ch = S_GET(); + } + if (ch != '\n') { + // Got a backslash followed by whitespace, followed by some char + // before newline + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + // fallthru + } + if (ch == '\n') { + // got a line-ending backslash + // - skip all whitespaces + while (scan_matchany(sp, " \t\n")) { + S_GET(); + } + continue; + } + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + tok->str.len = sp->cur - tok->str.ptr; + tok->u.escp = escp; + + assert(S_MATCH3('"')); + S_GET(), S_GET(), S_GET(); + return 0; +} + +static int scan_string(scanner_t *sp, token_t *tok) { + assert(S_MATCH('"')); + if (S_MATCH3('"')) { + return scan_multiline_string(sp, tok); + } + S_GET(); // skip opening " + + // scan until closing " + *tok = mktoken(sp, TOK_STRING); + const char *escp = NULL; + while (!S_MATCH('"')) { + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); + } + // If non-escaped char ... + if (ch != '\\') { + if (!(is_valid_char(ch) || ch == ' ' || ch == '\t')) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + continue; + } + // ch is backslash + if (!escp) { + escp = sp->cur - 1; + assert(*escp == '\\'); + } + + // handle escape char + ch = S_GET(); + if (ch && strchr("btnfre\"\\", ch)) { + // skip \b, \t, \n, \f, \r, \e, \", \\ . + continue; + } + int top = 0; + switch (ch) { + case 'x': + top = 2; + break; + case 'u': + top = 4; + break; + case 'U': + top = 8; + break; + default: + return SETERROR(sp->ebuf, sp->lineno, "bad escape char in string"); + } + for (int i = 0; i < top; i++) { + if (!is_hex_char(S_GET())) { + return SETERROR(sp->ebuf, sp->lineno, "expect %d hex digits after \\%c", + top, ch); + } + } + } + tok->str.len = sp->cur - tok->str.ptr; + tok->u.escp = escp; + + assert(S_MATCH('"')); + S_GET(); // skip the terminating " + return 0; +} + +static int scan_multiline_litstring(scanner_t *sp, token_t *tok) { + assert(S_MATCH3('\'')); + S_GET(), S_GET(), S_GET(); // skip opening ''' + + // According to spec: trim first newline after ''' + if (S_MATCH('\n')) { + S_GET(); + } + + // scan until terminating ''' + *tok = mktoken(sp, TOK_MLLITSTRING); + while (1) { + if (S_MATCH3('\'')) { + if (S_MATCH4('\'')) { + // special case... '''abcd '''' -> (abcd ') + // but sequences of 3 or more single quotes are not allowed + if (S_MATCH6('\'')) { + return SETERROR(sp->ebuf, sp->lineno, + "sequences of 3 or more single quotes"); + } else { + ; // no problem + } + } else { + break; // found terminating ''' + } + } + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, + "unterminated multiline lit string"); + } + if (!(is_valid_char(ch) || (ch && strchr(" \t\n", ch)))) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + } + tok->str.len = sp->cur - tok->str.ptr; + + assert(S_MATCH3('\'')); + S_GET(), S_GET(), S_GET(); + return 0; +} + +static int scan_litstring(scanner_t *sp, token_t *tok) { + assert(S_MATCH('\'')); + if (S_MATCH3('\'')) { + return scan_multiline_litstring(sp, tok); + } + S_GET(); // skip opening ' + + // scan until closing ' + *tok = mktoken(sp, TOK_LITSTRING); + while (!S_MATCH('\'')) { + int ch = S_GET(); + if (ch == TOK_FIN) { + return SETERROR(sp->ebuf, sp->lineno, "unterminated string"); + } + if (!(is_valid_char(ch) || ch == '\t')) { + return SETERROR(sp->ebuf, sp->lineno, "invalid char in string"); + } + } + tok->str.len = sp->cur - tok->str.ptr; + assert(S_MATCH('\'')); + S_GET(); + return 0; +} + +static bool is_valid_date(int year, int month, int day) { + if (!(1 <= year)) { + return false; + } + if (!(1 <= month && month <= 12)) { + return false; + } + int is_leap_year = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + int days_in_month[] = { + 31, 28 + is_leap_year, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + return (1 <= day && day <= days_in_month[month - 1]); +} + +static bool is_valid_time(int hour, int minute, int sec, int usec) { + if (!(0 <= hour && hour <= 23)) { + return false; + } + if (!(0 <= minute && minute <= 59)) { + return false; + } + if (!(0 <= sec && sec <= 59)) { + return false; + } + if (!(0 <= usec)) { + return false; + } + return true; +} + +static bool is_valid_timezone(int minute) { + minute = (minute < 0 ? -minute : minute); + int hour = minute / 60; + minute = minute % 60; + if (!(0 <= hour && hour <= 23)) { + return false; + } + if (!(0 <= minute && minute < 60)) { + return false; + } + return true; +} + +// Read an int (without signs) from the string p. +static int read_int(const char *p, int *ret) { + const char *pp = p; + int val = 0; + for (; isdigit(*p); p++) { + val = val * 10u + (*p - '0'); + if (val < 0) { + return 0; // overflowed + } + } + *ret = val; + return p - pp; +} + +// Read a date as YYYY-MM-DD from p[]. Return #bytes consumed. +static int read_date(const char *p, int *year, int *month, int *day) { + const char *pp = p; + int n; + n = read_int(p, year); + if (n != 4 || p[4] != '-') { + return 0; + } + n = read_int(p += n + 1, month); + if (n != 2 || p[2] != '-') { + return 0; + } + n = read_int(p += n + 1, day); + if (n != 2) { + return 0; + } + p += 2; + assert(p - pp == 10); + return p - pp; +} + +// Read a time as HH:MM:SS.subsec from p[]. Return #bytes consumed. +static int read_time(const char *p, int *hour, int *minute, int *second, + int *usec) { + const char *pp = p; + int n; + *hour = *minute = *second = *usec = 0; + // scan hours + n = read_int(p, hour); + if (n != 2 || p[2] != ':') { + return 0; + } + p += 3; + + // scan minutes + n = read_int(p, minute); + if (n != 2) { + return 0; + } + if (p[2] != ':') { + // seconds are optional in v1.1 + p += 2; + return p - pp; + } + p += 3; + + // scan seconds + n = read_int(p, second); + if (n != 2) { + return 0; + } + p += 2; + + if (*p != '.') { + return p - pp; + } + p++; // skip the period + if (!isdigit(*p)) { + // trailing period + return 0; + } + int micro_factor = 100000; + while (isdigit(*p) && micro_factor) { + *usec += (*p - '0') * micro_factor; + micro_factor /= 10; + p++; + } + return p - pp; +} + +// Reads a timezone from p[]. Return #bytes consumed. +// tzhours and tzminutes restricted to 2-char integers only. +static int read_tzone(const char *p, char *tzsign, int *tzhour, int *tzminute) { + const char *pp = p; + + // Default values + *tzhour = *tzminute = 0; + *tzsign = '+'; + + // Look for Zulu + if (*p == 'Z' || *p == 'z') { + return 1; // done! tz is +00:00. + } + + // Look for +/- + *tzsign = *p++; + if (!(*tzsign == '+' || *tzsign == '-')) { + return 0; + } + + // Look for HH:MM + int n; + n = read_int(p, tzhour); + if (n != 2 || p[2] != ':') { + return 0; + } + n = read_int(p += 3, tzminute); + if (n != 2) { + return 0; + } + p += 2; + return p - pp; +} + +static int scan_time(scanner_t *sp, token_t *tok) { + int lineno = sp->lineno; + char buffer[20]; + scan_copystr(sp, buffer, sizeof(buffer)); + + char *p = buffer; + int hour, minute, sec, usec; + int len = read_time(p, &hour, &minute, &sec, &usec); + if (len == 0) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + + *tok = mktoken(sp, TOK_TIME); + tok->str.len = len; + sp->cur += len; + tok->u.tsval.year = -1; + tok->u.tsval.month = -1; + tok->u.tsval.day = -1; + tok->u.tsval.hour = hour; + tok->u.tsval.minute = minute; + tok->u.tsval.sec = sec; + tok->u.tsval.usec = usec; + tok->u.tsval.tz = -1; + return 0; +} + +static int scan_timestamp(scanner_t *sp, token_t *tok) { + int year, month, day, hour, minute, sec, usec, tz; + int n; + // make a copy of sp->cur into buffer to ensure NUL terminated string + char buffer[80]; + scan_copystr(sp, buffer, sizeof(buffer)); + + toktyp_t toktyp = TOK_FIN; + int lineno = sp->lineno; + const char *p = buffer; + if (isdigit(p[0]) && isdigit(p[1]) && p[2] == ':') { + year = month = day = hour = minute = sec = usec = tz = -1; + n = read_time(buffer, &hour, &minute, &sec, &usec); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + toktyp = TOK_TIME; + p += n; + goto done; + } + + year = month = day = hour = minute = sec = usec = tz = -1; + n = read_date(p, &year, &month, &day); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + toktyp = TOK_DATE; + p += n; + + // Check if there is a time component + if (!((p[0] == 'T' || p[0] == ' ' || p[0] == 't') && isdigit(p[1]) && + isdigit(p[2]) && p[3] == ':')) { + goto done; // date only + } + + // Read the time + n = read_time(p += 1, &hour, &minute, &sec, &usec); + if (!n) { + return SETERROR(sp->ebuf, lineno, "invalid timestamp"); + } + toktyp = TOK_DATETIME; + p += n; + + // Read the (optional) timezone + char tzsign; + int tzhour, tzminute; + n = read_tzone(p, &tzsign, &tzhour, &tzminute); + if (n == 0) { + goto done; // datetime only + } + toktyp = TOK_DATETIMETZ; + p += n; + + // Check tzminute range. This must be done here instead of is_valid_timezone() + // because we combine tzhour and tzminute into tz (by minutes only). + if (!(0 <= tzminute && tzminute < 60)) { + return SETERROR(sp->ebuf, lineno, "invalid timezone"); + } + tz = (tzhour * 60 + tzminute) * (tzsign == '-' ? -1 : 1); + goto done; // datetimetz + +done: + *tok = mktoken(sp, toktyp); + n = p - buffer; + tok->str.len = n; + sp->cur += n; + + tok->u.tsval.year = year; + tok->u.tsval.month = month; + tok->u.tsval.day = day; + tok->u.tsval.hour = hour; + tok->u.tsval.minute = minute; + tok->u.tsval.sec = sec; + tok->u.tsval.usec = usec; + tok->u.tsval.tz = tz; + + // Do some error checks based on type + switch (tok->toktyp) { + case TOK_TIME: + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + break; + case TOK_DATE: + if (!is_valid_date(year, month, day)) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + break; + case TOK_DATETIME: + case TOK_DATETIMETZ: + if (!is_valid_date(year, month, day)) { + return SETERROR(sp->ebuf, lineno, "invalid date"); + } + if (!is_valid_time(hour, minute, sec, usec)) { + return SETERROR(sp->ebuf, lineno, "invalid time"); + } + if (tok->toktyp == TOK_DATETIMETZ && !is_valid_timezone(tz)) { + return SETERROR(sp->ebuf, lineno, "invalid timezone"); + } + break; + default: + assert(0); + return SETERROR(sp->ebuf, lineno, "internal error"); + } + + return 0; +} + +// Given a toml number (int and float) in buffer[]: +// 1. squeeze out '_' +// 2. check for syntax restrictions +static int process_numstr(char *buffer, int base, const char **reason) { + // squeeze out _ + char *q = strchr(buffer, '_'); + if (q) { + for (int i = q - buffer; buffer[i]; i++) { + if (buffer[i] != '_') { + *q++ = buffer[i]; + continue; + } + int left = (i == 0) ? 0 : buffer[i - 1]; + int right = buffer[i + 1]; + if (!isdigit(left) && !(base == 16 && is_hex_char(left))) { + *reason = "underscore only allowed between digits"; + return -1; + } + if (!isdigit(right) && !(base == 16 && is_hex_char(right))) { + *reason = "underscore only allowed between digits"; + return -1; + } + } + *q = 0; + } + + // decimal points must be surrounded by digits. Also, convert to lowercase. + for (int i = 0; buffer[i]; i++) { + if (buffer[i] == '.') { + if (i == 0 || !isdigit(buffer[i - 1]) || !isdigit(buffer[i + 1])) { + *reason = "decimal point must be surrounded by digits"; + return -1; + } + } else if ('A' <= buffer[i] && buffer[i] <= 'Z') { + buffer[i] = tolower(buffer[i]); + } + } + + if (base == 10) { + // check for leading 0: '+01' is an error! + q = buffer; + q += (*q == '+' || *q == '-') ? 1 : 0; + if (q[0] == '0' && isdigit(q[1])) { + *reason = "leading 0 in numbers"; + return -1; + } + } + + return 0; +} + +static int scan_float(scanner_t *sp, token_t *tok) { + char buffer[50]; // need to accomodate "9_007_199_254_740_991.0" + scan_copystr(sp, buffer, sizeof(buffer)); + + int lineno = sp->lineno; + char *p = buffer; + p += (*p == '+' || *p == '-') ? 1 : 0; + if (0 == memcmp(p, "nan", 3) || (0 == memcmp(p, "inf", 3))) { + p += 3; + } else { + p += strspn(p, "_0123456789eE.+-"); + } + int len = p - buffer; + buffer[len] = 0; + + const char *reason; + if (process_numstr(buffer, 10, &reason)) { + return SETERROR(sp->ebuf, lineno, reason); + } + + errno = 0; + char *q; + double fp64 = strtod(buffer, &q); + if (errno || *q || q == buffer) { + return SETERROR(sp->ebuf, lineno, "error parsing float"); + } + + *tok = mktoken(sp, TOK_FLOAT); + tok->u.fp64 = fp64; + tok->str.len = len; + sp->cur += len; + return 0; +} + +static int scan_number(scanner_t *sp, token_t *tok) { + const char *reason; + char buffer[50]; // need to accomodate "9_007_199_254_740_991.0" + scan_copystr(sp, buffer, sizeof(buffer)); + + char *p = buffer; + int lineno = sp->lineno; + // process %0x, %0o or %0b integers + if (p[0] == '0') { + const char *span = 0; + int base = 0; + switch (p[1]) { + case 'x': + base = 16; + span = "_0123456789abcdefABCDEF"; + break; + case 'o': + base = 8; + span = "_01234567"; + break; + case 'b': + base = 2; + span = "_01"; + break; + } + if (base) { + p += 2; + p += strspn(p, span); + int len = p - buffer; + buffer[len] = 0; + + if (process_numstr(buffer + 2, base, &reason)) { + return SETERROR(sp->ebuf, lineno, reason); + } + + // use strtoll to obtain the value + *tok = mktoken(sp, TOK_INTEGER); + errno = 0; + char *q; + tok->u.int64 = strtoll(buffer + 2, &q, base); + if (errno || *q || q == buffer + 2) { + return SETERROR(sp->ebuf, lineno, "error parsing integer"); + } + tok->str.len = len; + sp->cur += len; + return 0; + } + } + + // handle inf/nan + if (*p == '+' || *p == '-') { + p++; + } + if (*p == 'i' || *p == 'n') { + return scan_float(sp, tok); + } + + // regular int or float + p = buffer; + p += strspn(p, "0123456789_+-.eE"); + int len = p - buffer; + buffer[len] = 0; + + if (process_numstr(buffer, 10, &reason)) { + return SETERROR(sp->ebuf, lineno, reason); + } + + *tok = mktoken(sp, TOK_INTEGER); + errno = 0; + char *q; + tok->u.int64 = strtoll(buffer, &q, 10); + if (errno || *q || q == buffer) { + if (*q && strchr(".eE", *q)) { + return scan_float(sp, tok); // try to fit a float + } + return SETERROR(sp->ebuf, lineno, "error parsing integer"); + } + + tok->str.len = len; + sp->cur += len; + return 0; +} + +static int scan_bool(scanner_t *sp, token_t *tok) { + char buffer[10]; + scan_copystr(sp, buffer, sizeof(buffer)); + + int lineno = sp->lineno; + bool val = false; + const char *p = buffer; + if (0 == strncmp(p, "true", 4)) { + val = true; + p += 4; + } else if (0 == strncmp(p, "false", 5)) { + val = false; + p += 5; + } else { + return SETERROR(sp->ebuf, lineno, "invalid boolean value"); + } + if (*p && !strchr("# \r\n\t,}]", *p)) { + return SETERROR(sp->ebuf, lineno, "invalid boolean value"); + } + + int len = p - buffer; + *tok = mktoken(sp, TOK_BOOL); + tok->u.b1 = val; + tok->str.len = len; + sp->cur += len; + return 0; +} + +// Check if the next token may be TIME +static inline bool test_time(const char *p, const char *endp) { + return &p[2] < endp && isdigit(p[0]) && isdigit(p[1]) && p[2] == ':'; +} + +// Check if the next token may be DATE +static inline bool test_date(const char *p, const char *endp) { + return &p[4] < endp && isdigit(p[0]) && isdigit(p[1]) && isdigit(p[2]) && + isdigit(p[3]) && p[4] == '-'; +} + +// Check if the next token may be BOOL +static inline bool test_bool(const char *p, const char *endp) { + return &p[0] < endp && (*p == 't' || *p == 'f'); +} + +// Check if the next token may be NUMBER +static bool test_number(const char *p, const char *endp) { + if (&p[0] < endp && *p && strchr("0123456789+-._", *p)) { + return true; + } + if (&p[2] < endp) { + if (0 == memcmp(p, "nan", 3) || 0 == memcmp(p, "inf", 3)) { + return true; + } + } + return false; +} + +// Scan a literal that is not a string +static int scan_nonstring_literal(scanner_t *sp, token_t *tok) { + int lineno = sp->lineno; + if (test_time(sp->cur, sp->endp)) { + return scan_time(sp, tok); + } + + if (test_date(sp->cur, sp->endp)) { + return scan_timestamp(sp, tok); + } + + if (test_bool(sp->cur, sp->endp)) { + return scan_bool(sp, tok); + } + + if (test_number(sp->cur, sp->endp)) { + return scan_number(sp, tok); + } + return SETERROR(sp->ebuf, lineno, "invalid value"); +} + +// Scan a literal +static int scan_literal(scanner_t *sp, token_t *tok) { + *tok = mktoken(sp, TOK_LIT); + const char *p = sp->cur; + while (p < sp->endp && (isalnum(*p) || *p == '_' || *p == '-')) { + p++; + } + tok->str.len = p - tok->str.ptr; + sp->cur = p; + return 0; +} + +// Save the current state of the scanner +static scanner_state_t scan_mark(scanner_t *sp) { + scanner_state_t mark; + mark.sp = sp; + mark.cur = sp->cur; + mark.lineno = sp->lineno; + return mark; +} + +// Restore the scanner state to a previously saved state +static void scan_restore(scanner_t *sp, scanner_state_t mark) { + assert(mark.sp == sp); + sp->cur = mark.cur; + sp->lineno = mark.lineno; +} + +// Return the next token +static int scan_next(scanner_t *sp, bool keymode, token_t *tok) { +again: + *tok = mktoken(sp, TOK_FIN); + + int ch = S_GET(); + if (ch == TOK_FIN) { + return 0; + } + + tok->str.len = 1; + switch (ch) { + case '\n': + tok->toktyp = TOK_ENDL; + break; + + case ' ': + case '\t': + goto again; // skip whitespace + + case '#': + // comment: skip until newline + while (!S_MATCH('\n')) { + ch = S_GET(); + if (ch == TOK_FIN) + break; + if ((0 <= ch && ch <= 0x8) || (0x0a <= ch && ch <= 0x1f) || + (ch == 0x7f)) { + return SETERROR(sp->ebuf, sp->lineno, "bad control char in comment"); + } + } + goto again; // skip comment + + case '.': + tok->toktyp = TOK_DOT; + break; + + case '=': + tok->toktyp = TOK_EQUAL; + break; + + case ',': + tok->toktyp = TOK_COMMA; + break; + + case '[': + tok->toktyp = TOK_LBRACK; + if (keymode && S_MATCH('[')) { + S_GET(); + tok->toktyp = TOK_LLBRACK; + tok->str.len = 2; + } + break; + + case ']': + tok->toktyp = TOK_RBRACK; + if (keymode && S_MATCH(']')) { + S_GET(); + tok->toktyp = TOK_RRBRACK; + tok->str.len = 2; + } + break; + + case '{': + tok->toktyp = TOK_LBRACE; + break; + + case '}': + tok->toktyp = TOK_RBRACE; + break; + + case '"': + sp->cur--; + DO(scan_string(sp, tok)); + break; + + case '\'': + sp->cur--; + DO(scan_litstring(sp, tok)); + break; + + default: + sp->cur--; + DO(keymode ? scan_literal(sp, tok) : scan_nonstring_literal(sp, tok)); + break; + } + + return 0; +} + +// Check for stack overflow due to excessive number of brackets or braces +static int check_overflow(scanner_t *sp, token_t *tok) { + switch (tok->toktyp) { + case TOK_LBRACK: + sp->bracket_level++; + if (sp->bracket_level > BRACKET_LEVEL_MAX) { + return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); + } + break; + case TOK_RBRACK: + sp->bracket_level--; + break; + case TOK_LBRACE: + sp->brace_level++; + if (sp->brace_level > BRACE_LEVEL_MAX) { + return SETERROR(sp->ebuf, sp->lineno, "stack overflow"); + } + break; + case TOK_RBRACE: + sp->brace_level--; + break; + default: + break; + } + return 0; +} + +static int scan_key(scanner_t *sp, token_t *tok) { + if (sp->errmsg) { + return -1; + } + if (scan_next(sp, true, tok) || check_overflow(sp, tok)) { + sp->errmsg = sp->ebuf.ptr; + return -1; + } + return 0; +} + +static int scan_value(scanner_t *sp, token_t *tok) { + if (sp->errmsg) { + return -1; + } + if (scan_next(sp, false, tok) || check_overflow(sp, tok)) { + sp->errmsg = sp->ebuf.ptr; + return -1; + } + return 0; +} + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +static int utf8_to_ucs(const char *orig, int len, uint32_t *ret) { + const unsigned char *buf = (const unsigned char *)orig; + unsigned i = *buf++; + uint32_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) + return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) + return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) + return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) + return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + if (0) { + // NOTE: these code points taking more than 4 bytes are not supported + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) + return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) + return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) + return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char *)buf - orig; + } + } + + return -1; +} + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +static int ucs_to_utf8(uint32_t code, char buf[4]) { + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 + */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + /* + * https://github.com/toml-lang/toml-test/issues/165 + * [0xd800, 0xdfff] and [0xfffe, 0xffff] are implicitly allowed by TOML, so + * we disable the check. + */ + if (0) { + if (0xd800 <= code && code <= 0xdfff) + return -1; + if (0xfffe <= code && code <= 0xffff) + return -1; + } + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code <= 0x7F) { + buf[0] = (unsigned char)code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = (unsigned char)(0xc0 | (code >> 6)); + buf[1] = (unsigned char)(0x80 | (code & 0x3f)); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = (unsigned char)(0xe0 | (code >> 12)); + buf[1] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[2] = (unsigned char)(0x80 | (code & 0x3f)); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = (unsigned char)(0xf0 | (code >> 18)); + buf[1] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[3] = (unsigned char)(0x80 | (code & 0x3f)); + return 4; + } + +#ifdef UNDEF + if (0) { + // NOTE: these code points taking more than 4 bytes are not supported + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = (unsigned char)(0xf8 | (code >> 24)); + buf[1] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[4] = (unsigned char)(0x80 | (code & 0x3f)); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = (unsigned char)(0xfc | (code >> 30)); + buf[1] = (unsigned char)(0x80 | ((code >> 24) & 0x3f)); + buf[2] = (unsigned char)(0x80 | ((code >> 18) & 0x3f)); + buf[3] = (unsigned char)(0x80 | ((code >> 12) & 0x3f)); + buf[4] = (unsigned char)(0x80 | ((code >> 6) & 0x3f)); + buf[5] = (unsigned char)(0x80 | (code & 0x3f)); + return 6; + } + } +#endif + + return -1; +} diff --git a/libs/tomlc17/tomlc17.h b/libs/tomlc17/tomlc17.h new file mode 100644 index 0000000000..de60fcac13 --- /dev/null +++ b/libs/tomlc17/tomlc17.h @@ -0,0 +1,190 @@ +/* Copyright (c) 2024-2026, CK Tan. + * https://github.com/cktan/tomlc17/blob/main/LICENSE + */ +#ifndef TOMLC17_H +#define TOMLC17_H + +/* + * USAGE: + * + * 1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex() + * 2. Check result.ok + * 3. Use toml_get() or toml_seek() to query and traverse the + * result.toptab + * 4. Call toml_free() to release resources. + * + */ + +#include +#include +#include + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +enum toml_type_t { + TOML_UNKNOWN = 0, + TOML_STRING, + TOML_INT64, + TOML_FP64, + TOML_BOOLEAN, + TOML_DATE, + TOML_TIME, + TOML_DATETIME, + TOML_DATETIMETZ, + TOML_ARRAY, + TOML_TABLE, +}; +typedef enum toml_type_t toml_type_t; + +/* This is a Node in a Tree that represents a toml document rooted + * at toml_result_t::toptab. + */ +typedef struct toml_datum_t toml_datum_t; +struct toml_datum_t { + toml_type_t type; + uint32_t flag; // internal + union { + const char *s; // same as str.ptr; use if there are no NUL in string. + struct { + const char *ptr; // NUL terminated string + int len; // length excluding the terminating NUL. + } str; + int64_t int64; // integer + double fp64; // float + bool boolean; + struct { // date, time + int16_t year, month, day; + int16_t hour, minute, second; + int32_t usec; + int16_t tz; // in minutes + } ts; + struct { // array + int32_t size; // count elem + toml_datum_t *elem; // elem[] + } arr; + struct { // table + int32_t size; // count key + const char **key; // key[] + int *len; // len[] + toml_datum_t *value; // value[] + } tab; + } u; +}; + +/* Result returned by toml_parse() */ +typedef struct toml_result_t toml_result_t; +struct toml_result_t { + bool ok; // success flag + toml_datum_t toptab; // valid if ok + char errmsg[200]; // valid if not ok + void *__internal; // do not use +}; + +/** + * Parse a toml document. Returns a toml_result which must be freed + * using toml_free() eventually. + * + * IMPORTANT: src[] must be a NUL terminated string! The len parameter + * does not include the NUL terminator. + */ +TOML_EXTERN toml_result_t toml_parse(const char *src, int len); + +/** + * Parse a toml file. Returns a toml_result which must be freed + * using toml_free() eventually. + * + * IMPORTANT: you are still responsible to fclose(fp). + */ +TOML_EXTERN toml_result_t toml_parse_file(FILE *fp); + +/** + * Parse a toml file. Returns a toml_result which must be freed + * using toml_free() eventually. + */ +TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname); + +/** + * Release the result. + */ +TOML_EXTERN void toml_free(toml_result_t result); + +/** + * Find a key in a toml_table. Return the value of the key if found, + * or a TOML_UNKNOWN otherwise. + */ +TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key); + +/** + * Locate a value starting from a toml_table. Return the value of the key if + * found, or a TOML_UNKNOWN otherwise. + * + * Note: the multipart-key is separated by DOT, and must not have any escape + * chars. The maximum length of the multipart_key must not exceed 127 bytes. + */ +TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table, + const char *multipart_key); + +/** + * OBSOLETE: use toml_get() instead. + * Find a key in a toml_table. Return the value of the key if found, + * or a TOML_UNKNOWN otherwise. ( + */ +static inline toml_datum_t toml_table_find(toml_datum_t table, + const char *key) { + return toml_get(table, key); +} + +/** + * Override values in r1 using r2. Return a new result. All results + * (i.e., r1, r2 and the returned result) must be freed using toml_free() + * after use. + * + * LOGIC: + * ret = copy of r1 + * for each item x in r2: + * if x is not in ret: + * override + * elif x in ret is NOT of the same type: + * override + * elif x is an array of tables: + * append r2.x to ret.x + * elif x is a table: + * merge r2.x to ret.x + * else: + * override + */ +TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1, + const toml_result_t *r2); + +/** + * Check if two results are the same. Dictinary and array orders are + * sensitive. + */ +TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2); + +/* Options that override tomlc17 defaults globally */ +typedef struct toml_option_t toml_option_t; +struct toml_option_t { + bool check_utf8; // Check all chars are valid utf8; default: false. + void *(*mem_realloc)(void *ptr, size_t size); // default: realloc() + void (*mem_free)(void *ptr); // default: free() +}; + +/** + * Get the default options. IF NECESSARY, use this to initialize + * toml_option_t and override values before calling + * toml_set_option(). + */ +TOML_EXTERN toml_option_t toml_default_option(void); + +/** + * Set toml options globally. Do this ONLY IF you are not satisfied with the + * defaults. + */ +TOML_EXTERN void toml_set_option(toml_option_t opt); + +#endif // TOMLC17_H diff --git a/libs/tomlc17/update.sh b/libs/tomlc17/update.sh new file mode 100755 index 0000000000..8fd15f1f7e --- /dev/null +++ b/libs/tomlc17/update.sh @@ -0,0 +1,9 @@ +#! /usr/bin/env bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +# tomlcpp.hpp requires C++20. +for file in tomlc17.c tomlc17.h # tomlcpp.hpp +do + wget -O "${file}" "https://github.com/cktan/tomlc17/raw/refs/heads/main/src/${file}" +done diff --git a/src.cmake b/src.cmake index bd5e8b47e0..f48557d5f6 100644 --- a/src.cmake +++ b/src.cmake @@ -185,6 +185,8 @@ set(ENGINELIST ${ENGINE_DIR}/framework/CrashDump.cpp ${ENGINE_DIR}/framework/CvarSystem.cpp ${ENGINE_DIR}/framework/CvarSystem.h + ${ENGINE_DIR}/framework/GameInfo.h + ${ENGINE_DIR}/framework/GameInfo.cpp ${ENGINE_DIR}/framework/LogSystem.cpp ${ENGINE_DIR}/framework/LogSystem.h ${ENGINE_DIR}/framework/OmpSystem.cpp diff --git a/src/common/Defs.h b/src/common/Defs.h index b672b20b42..b865ce8e52 100644 --- a/src/common/Defs.h +++ b/src/common/Defs.h @@ -31,32 +31,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef COMMON_DEFS_H_ #define COMMON_DEFS_H_ -#define PRODUCT_NAME "Unvanquished" -/** Case, No spaces */ -#define PRODUCT_NAME_UPPER "UNVANQUISHED" -/** No case, No spaces */ -#define PRODUCT_NAME_LOWER "unvanquished" - -#define PRODUCT_APPID "net.unvanquished.Unvanquished" - -#define PRODUCT_VERSION "0.56.0" - -/** Default base package */ -#define DEFAULT_BASE_PAK PRODUCT_NAME_LOWER - -/** URI scheme and server browser filter */ -#define GAMENAME_STRING "unv" -#define GAMENAME_FOR_MASTER PRODUCT_NAME_UPPER +#define ENGINE_NAME "Daemon Engine" +#define ENGINE_VERSION "0.56.0" +#define ENGINE_DATE __DATE__ #define MAX_MASTER_SERVERS 5 -#define MASTER1_SERVER_NAME "master.unvanquished.net" -#define MASTER2_SERVER_NAME "master2.unvanquished.net" -#define MASTER3_SERVER_NAME "" -#define MASTER4_SERVER_NAME "" -#define MASTER5_SERVER_NAME "" - -#define WWW_BASEURL "dl.unvanquished.net/pkg" - #define AUTOEXEC_NAME "autoexec.cfg" #define CONFIG_NAME "autogen.cfg" @@ -64,7 +43,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define TEAMCONFIG_NAME "teamconfig.cfg" #define UNNAMED_PLAYER "UnnamedPlayer" -#define UNNAMED_SERVER "Unnamed " PRODUCT_NAME " Server" /** file containing our RSA public and private keys */ #define RSAKEY_FILE "pubkey" diff --git a/src/common/FileSystem.cpp b/src/common/FileSystem.cpp index 45bd66b8b0..ca3ce3240c 100644 --- a/src/common/FileSystem.cpp +++ b/src/common/FileSystem.cpp @@ -2332,27 +2332,27 @@ std::string DefaultHomePath() #ifdef _WIN32 wchar_t buffer[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, buffer))) { - return Path::Build(Path::Build(Str::UTF16To8(buffer), "My Games"), PRODUCT_NAME); + return Path::Build(Path::Build(Str::UTF16To8(buffer), "My Games"), GameInfo::getInstance().windowsDirName()); } #elif defined(__APPLE__) const char* home = getenv("HOME"); if (home && home[0]) { - return Path::Build(Path::Build(Path::Build(home, "Library"), "Application Support"), PRODUCT_NAME); + return Path::Build(Path::Build(Path::Build(home, "Library"), "Application Support"), GameInfo::getInstance().macosDirName()); } #else const char* xdgDataHome_ = getenv("XDG_DATA_HOME"); if (xdgDataHome_ && xdgDataHome_[0]) { - return Path::Build(xdgDataHome_, PRODUCT_NAME_LOWER); + return Path::Build(xdgDataHome_, GameInfo::getInstance().xdgDirName()); } else { const char* home = getenv("HOME"); if (home && home[0]) { std::string xdgDataHome = Path::Build(Path::Build(home, ".local") ,"share"); - return Path::Build(xdgDataHome, PRODUCT_NAME_LOWER); + return Path::Build(xdgDataHome, GameInfo::getInstance().xdgDirName()); } } #endif - return ""; + return ""; } // Determine path to temporary directory diff --git a/src/engine/client/ClientApplication.cpp b/src/engine/client/ClientApplication.cpp index c1e6d38204..9fd2208c74 100644 --- a/src/engine/client/ClientApplication.cpp +++ b/src/engine/client/ClientApplication.cpp @@ -108,7 +108,7 @@ class ClientApplication : public Application { void Shutdown(bool error, Str::StringRef message) override { #if defined(_WIN32) || defined(BUILD_GRAPHICAL_CLIENT) if (error && client_errorPopup.Get()) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, PRODUCT_NAME, message.c_str(), window); + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, ENGINE_NAME, message.c_str(), window); } #endif diff --git a/src/engine/client/cl_console.cpp b/src/engine/client/cl_console.cpp index 9c7a2d4bdf..39278f352c 100644 --- a/src/engine/client/cl_console.cpp +++ b/src/engine/client/cl_console.cpp @@ -681,8 +681,8 @@ void Con_DrawAboutText() //ANIMATION_TYPE_FADE but also ANIMATION_TYPE_SCROLL_DOWN needs this, latter, since it might otherwise scroll out the console color.SetAlpha( 0.66f * consoleState.currentAnimationFraction ); - Con_DrawRightFloatingTextLine( 0, color, Q3_VERSION ); - Con_DrawRightFloatingTextLine( 1, color, Q3_ENGINE ); + Con_DrawRightFloatingTextLine( 0, color, PRODUCT_NAME_VERSION ); + Con_DrawRightFloatingTextLine( 1, color, ENGINE_NAME_VERSION ); } /* diff --git a/src/engine/client/cl_main.cpp b/src/engine/client/cl_main.cpp index f80b735470..65c9b57582 100644 --- a/src/engine/client/cl_main.cpp +++ b/src/engine/client/cl_main.cpp @@ -983,9 +983,10 @@ void CL_Connect_f() } // Skip the URI scheme. - if ( !Q_strnicmp( server, URI_SCHEME, URI_SCHEME_LENGTH ) ) + std::string uri_scheme = Str::Format("%s://", GameInfo::getInstance().uriProtocol()); + if ( !Q_strnicmp( server, uri_scheme.c_str(), uri_scheme.length() ) ) { - server += URI_SCHEME_LENGTH; + server += uri_scheme.length(); } // Set and skip the password. diff --git a/src/engine/client/cl_serverlist.cpp b/src/engine/client/cl_serverlist.cpp index 09f99d2e15..84e4fe5947 100644 --- a/src/engine/client/cl_serverlist.cpp +++ b/src/engine/client/cl_serverlist.cpp @@ -49,7 +49,8 @@ Maryland 20850 USA. static Log::Logger serverInfoLog("client.serverinfo", ""); static Cvar::Cvar cl_gamename( - "cl_gamename", "game name for master server queries", Cvar::TEMPORARY, GAMENAME_FOR_MASTER); + "cl_gamename", "game name for master server queries", Cvar::TEMPORARY, + GameInfo::getInstance().masterGameName()); static Cvar::Range> cl_maxPing( "cl_maxPing", "ping timeout for server list", Cvar::NONE, 800, 100, 9999); @@ -544,7 +545,8 @@ void CL_ServerInfoPacket( const netadr_t& from, msg_t *msg ) // Arnout: if this isn't the correct game, ignore it gameName = Info_ValueForKey( infoString, "gamename" ); - if ( !gameName[ 0 ] || Q_stricmp( gameName, GAMENAME_STRING ) ) + if ( !gameName[ 0 ] + || Q_stricmp( gameName, GameInfo::getInstance().serverGameName().c_str() ) ) { serverInfoLog.Verbose( "Different game info packet: %s", infoString ); return; diff --git a/src/engine/client/dl_main.cpp b/src/engine/client/dl_main.cpp index 0044c64794..2686fb5d68 100644 --- a/src/engine/client/dl_main.cpp +++ b/src/engine/client/dl_main.cpp @@ -167,8 +167,8 @@ if ((err = curl_easy_setopt(request_, option, value)) != CURLE_OK) { \ return false; \ } - SETOPT( CURLOPT_USERAGENT, Str::Format( "%s %s", PRODUCT_NAME "/" PRODUCT_VERSION, curl_version() ).c_str() ) - SETOPT( CURLOPT_REFERER, Str::Format("%s%s", URI_SCHEME, Cvar::GetValue("cl_currentServerIP")).c_str() ) + SETOPT( CURLOPT_USERAGENT, Str::Format( "%s %s", ENGINE_NAME "/" ENGINE_VERSION, curl_version() ).c_str() ) + SETOPT( CURLOPT_REFERER, Str::Format("%s://%s", GameInfo::getInstance().uriProtocol().c_str(), Cvar::GetValue("cl_currentServerIP")).c_str() ) SETOPT( CURLOPT_URL, url.c_str() ) #if CURL_AT_LEAST_VERSION(7, 85, 0) SETOPT( CURLOPT_PROTOCOLS_STR, "http" ) diff --git a/src/engine/framework/CvarSystem.cpp b/src/engine/framework/CvarSystem.cpp index e9b2acb717..785f08ff92 100644 --- a/src/engine/framework/CvarSystem.cpp +++ b/src/engine/framework/CvarSystem.cpp @@ -319,6 +319,24 @@ namespace Cvar { return result; } + void SetDefaultValue(const std::string &cvarName, const std::string& defaultValue) { + CvarMap& cvars = GetCvarMap(); + auto it = cvars.find(cvarName); + cvarRecord_t *cvar = it->second; + GetCCvar(cvarName, *cvar); + + /* HACK: Rewrite me when this issue is fixed: + https://github.com/DaemonEngine/Daemon/issues/590 + Until that the cvar->ccvar.modified value can't be trusted. */ + + cvar->resetValue = defaultValue; + cvar->ccvar.resetString = CopyString(defaultValue.c_str()); + + if (!Q_stricmp(cvar->value.c_str(), "")) { + SetValue(cvarName, defaultValue); + } + } + bool Register(CvarProxy* proxy, const std::string& name, std::string description, int flags, const std::string& defaultValue) { CvarMap& cvars = GetCvarMap(); cvarRecord_t* cvar; diff --git a/src/engine/framework/CvarSystem.h b/src/engine/framework/CvarSystem.h index 92952154e2..117dea71ff 100644 --- a/src/engine/framework/CvarSystem.h +++ b/src/engine/framework/CvarSystem.h @@ -75,6 +75,9 @@ namespace Cvar { // Alter flags, returns true if the variable exists bool ClearFlags(const std::string& cvarName, int flags); + // Change default value. + void SetDefaultValue(const std::string &cvarName, const std::string& defaultValue); + // Used by statically defined cvar. bool Register(CvarProxy* proxy, const std::string& name, std::string description, int flags, const std::string& defaultValue); void Unregister(const std::string& cvarName); diff --git a/src/engine/framework/GameInfo.cpp b/src/engine/framework/GameInfo.cpp new file mode 100644 index 0000000000..4d32a74588 --- /dev/null +++ b/src/engine/framework/GameInfo.cpp @@ -0,0 +1,231 @@ +/* +=========================================================================== + +Daemon GPL Source Code +Copyright (C) 2017-2026, Daemon Developers + +This file is part of the Daemon GPL Source Code (Daemon Source Code). + +Daemon Source Code is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Daemon Source Code is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Daemon Source Code. If not, see . + +=========================================================================== +*/ + +#include +#include + +#include "framework/CvarSystem.h" +#include "framework/GameInfo.h" +#include "common/FileSystem.h" + +// tomlcpp.hpp requires C++20. +#include "tomlc17/tomlc17.h" + +GameInfo& GameInfo::getInstance() +{ + static GameInfo instance; + return instance; +} + +const std::string GameInfo::fileName = "daemon.ini"; + +inline void CheckTomlProperty(std::string name, toml_datum_t datum, toml_type_t type) +{ + if (datum.type != type) + { + Sys::Error("Missing or invalid “%s” property in “%s”.", name, GameInfo::fileName); + } +} + +void GameInfo::parse(std::string fname) +{ + if (!FS::RawPath::FileExists(fname)) + { + Sys::Error("Missing “%s” in libpath.", GameInfo::fileName); + } + + toml_result_t result = toml_parse_file_ex(fname.c_str()); + + if (!result.ok) + { + Sys::Error("Failed to read “%s”.", GameInfo::fileName); + } + + toml_datum_t toml_name = toml_seek(result.toptab, "gameinfo.name"); + CheckTomlProperty("name", toml_name, TOML_STRING); + _name = toml_name.u.s; + Cvar::SetDefaultValue("sv_hostname", "Unnamed " + _name + " Server"); + + toml_datum_t toml_version = toml_seek(result.toptab, "gameinfo.version"); + CheckTomlProperty("version", toml_version, TOML_STRING); + _version = toml_version.u.s; + + toml_datum_t toml_appId = toml_seek(result.toptab, "gameinfo.appId"); + CheckTomlProperty("appId", toml_appId, TOML_STRING); + _appId = toml_appId.u.s; + + toml_datum_t toml_windowsDirName = toml_seek(result.toptab, "gameinfo.windowsDirName"); + CheckTomlProperty("windowsDirName", toml_windowsDirName, TOML_STRING); + _windowsDirName = toml_windowsDirName.u.s; + + toml_datum_t toml_macosDirName = toml_seek(result.toptab, "gameinfo.macosDirName"); + CheckTomlProperty("macosDirName", toml_macosDirName, TOML_STRING); + _macosDirName = toml_macosDirName.u.s; + + toml_datum_t toml_xdgDirName = toml_seek(result.toptab, "gameinfo.xdgDirName"); + CheckTomlProperty("xdgDirName", toml_xdgDirName, TOML_STRING); + _xdgDirName = toml_xdgDirName.u.s; + + toml_datum_t toml_baseName = toml_seek(result.toptab, "gameinfo.baseName"); + CheckTomlProperty("baseName", toml_baseName, TOML_STRING); + _baseName = toml_baseName.u.s; + + toml_datum_t toml_basePak = toml_seek(result.toptab, "gameinfo.basePak"); + CheckTomlProperty("basePak", toml_basePak, TOML_STRING); + _basePak = toml_basePak.u.s; + Cvar::SetDefaultValue("fs_basepak", _basePak); + + toml_datum_t toml_masterServers = toml_seek(result.toptab, "gameinfo.masterServers"); + CheckTomlProperty("masterServers", toml_masterServers, TOML_ARRAY); + + for (int i = 0; i < toml_masterServers.u.arr.size; i++) + { + if ( i > MAX_MASTER_SERVERS ) + { + Log::Warn("Only %d masters server are supported.", MAX_MASTER_SERVERS); + break; + } + + toml_datum_t toml_masterServer = toml_masterServers.u.arr.elem[i]; + CheckTomlProperty(va("masterServers[%d]", i), toml_masterServer, TOML_STRING); + Cvar::SetDefaultValue(va("sv_master%d", i + 1), toml_masterServer.u.s); + } + + toml_datum_t toml_wwwBaseUrl = toml_seek(result.toptab, "gameinfo.wwwBaseUrl"); + CheckTomlProperty("wwwBaseUrl", toml_wwwBaseUrl, TOML_STRING); + _wwwBaseUrl = toml_wwwBaseUrl.u.s; + + toml_datum_t toml_masterGameName = toml_seek(result.toptab, "gameinfo.masterGameName"); + CheckTomlProperty("masterGameName", toml_masterGameName, TOML_STRING); + _masterGameName = toml_masterGameName.u.s; + + toml_datum_t toml_serverGameName = toml_seek(result.toptab, "gameinfo.serverGameName"); + CheckTomlProperty("serverGameName", toml_serverGameName, TOML_STRING); + _serverGameName = toml_serverGameName.u.s; + + toml_datum_t toml_uriProtocol = toml_seek(result.toptab, "gameinfo.uriProtocol"); + CheckTomlProperty("uriProtocol", toml_uriProtocol, TOML_STRING); + _uriProtocol = toml_uriProtocol.u.s; + + toml_free(result); +} + +std::string GameInfo::name() const +{ + return _name; +} + +std::string GameInfo::name_lower() const +{ + std::string temp = _name; + std::transform(temp.begin(), temp.end(), temp.begin(), ::tolower); + return temp; +} + +std::string GameInfo::name_upper() const +{ + std::string temp = _name; + std::transform(temp.begin(), temp.end(), temp.begin(), ::toupper); + return temp; +} + +std::string GameInfo::version() const +{ + return _version; +} + +std::string GameInfo::appId() const +{ + return _appId; +} + +std::string GameInfo::windowsDirName() const +{ + return _windowsDirName; +} + +std::string GameInfo::macosDirName() const +{ + return _macosDirName; +} + +std::string GameInfo::xdgDirName() const +{ + return _xdgDirName; +} + +std::string GameInfo::baseName() const +{ + return _baseName; +} + +std::string GameInfo::basePak() const +{ + return _basePak; +} + +std::string GameInfo::masterServer1() const +{ + return _masterServer1; +} + +std::string GameInfo::masterServer2() const +{ + return _masterServer2; +} + +std::string GameInfo::masterServer3() const +{ + return _masterServer3; +} + +std::string GameInfo::masterServer4() const +{ + return _masterServer4; +} + +std::string GameInfo::masterServer5() const +{ + return _masterServer5; +} + +std::string GameInfo::wwwBaseUrl() const +{ + return _wwwBaseUrl; +} + +std::string GameInfo::masterGameName() const +{ + return _masterGameName; +} + +std::string GameInfo::serverGameName() const +{ + return _serverGameName; +} + +std::string GameInfo::uriProtocol() const +{ + return _uriProtocol; +} diff --git a/src/engine/framework/GameInfo.h b/src/engine/framework/GameInfo.h new file mode 100644 index 0000000000..29fef27410 --- /dev/null +++ b/src/engine/framework/GameInfo.h @@ -0,0 +1,77 @@ +/* +=========================================================================== + +Daemon GPL Source Code +Copyright (C) 2017-26, Daemon Developers + +This file is part of the Daemon GPL Source Code (Daemon Source Code). + +Daemon Source Code is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Daemon Source Code is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Daemon Source Code. If not, see . + +=========================================================================== +*/ + +#ifndef GAMEINFO_H_ +#define GAMEINFO_H_ + +#include + + +class GameInfo +{ +public: + static GameInfo& getInstance(); + void parse(std::string fname); + static const std::string fileName; + std::string name() const; + std::string name_lower() const; + std::string name_upper() const; + std::string version() const; + std::string appId() const; + std::string windowsDirName() const; + std::string macosDirName() const; + std::string xdgDirName() const; + std::string baseName() const; + std::string basePak() const; + std::string masterServer1() const; + std::string masterServer2() const; + std::string masterServer3() const; + std::string masterServer4() const; + std::string masterServer5() const; + std::string wwwBaseUrl() const; + std::string masterGameName() const; + std::string serverGameName() const; + std::string uriProtocol() const; +private: + GameInfo() : _name("MISSING_NAME"), _version("UNKNOWN") {}; + std::string _name; + std::string _version; + std::string _appId; + std::string _windowsDirName; + std::string _macosDirName; + std::string _xdgDirName; + std::string _baseName; + std::string _basePak; + std::string _masterServer1; + std::string _masterServer2; + std::string _masterServer3; + std::string _masterServer4; + std::string _masterServer5; + std::string _wwwBaseUrl; + std::string _masterGameName; + std::string _serverGameName; + std::string _uriProtocol; +}; + +#endif // GAMEINFO_H_ diff --git a/src/engine/framework/System.cpp b/src/engine/framework/System.cpp index 4f697ce3be..6fc117b9e0 100644 --- a/src/engine/framework/System.cpp +++ b/src/engine/framework/System.cpp @@ -187,13 +187,13 @@ std::string GetSingletonSocketPath() Com_MD5Buffer(homePath.data(), homePath.size(), homePathHash, sizeof(homePathHash)); std::string suffix = homePathSuffix + "-" + homePathHash; #ifdef _WIN32 - return "\\\\.\\pipe\\" PRODUCT_NAME + suffix; + return std::string("\\\\.\\pipe\\") + GameInfo::getInstance().name() + suffix; #else // We use a temporary directory rather that using the homepath because // socket paths are limited to about 100 characters. This also avoids issues // when the homepath is on a network filesystem. return FS::Path::Build(FS::Path::Build( - FS::DefaultTempPath(), "." PRODUCT_NAME_LOWER + suffix), SINGLETON_SOCKET_BASENAME); + FS::DefaultTempPath(), std::string(".") + GameInfo::getInstance().baseName() + suffix), SINGLETON_SOCKET_BASENAME); #endif } @@ -724,22 +724,21 @@ static void ParseCmdline(int argc, char** argv, cmdlineArgs_t& cmdlineArgs) "Possible options are:\n" " -h, -help print this help and exit\n" " -v, -version print version and exit\n" - " -homepath set the path used for user-specific configuration files and downloaded dpk files\n" - " -libpath set the path containing additional executables and libraries\n" - " -pakpath add another path from which dpk files are loaded\n" - " -resetconfig reset all cvars and keybindings to their default value\n" + " -homepath set the path used for user-specific configuration files and downloaded dpk files\n" + " -libpath set the path containing additional executables and libraries\n" + " -pakpath add another path from which dpk files are loaded\n" + " -resetconfig reset all cvars and keybindings to their default value\n" " -noforward do not forward commands to an existing existance; instead exit with error\n" " -forward-only just forward commands; exit with error if no existing instance\n" #ifdef USE_CURSES - " -curses activate the curses interface\n" + " -curses activate the curses interface\n" #endif " -nocrashhandler disable catching SIGSEGV etc. (enable core dumps)\n" " -set set the value of a cvar\n"); - printf("%s", Application::GetTraits().supportsUri ? - " -connect " URI_SCHEME "
[:]>\n" - " connect to server at startup\n" : ""); - printf( - " + execute an ingame command after startup\n" + std::string helpUrl = std::string(" -connect ") + GameInfo::getInstance().uriProtocol() + std::string("
[:]>\n") + + std::string(" connect to server at startup\n"); + printf("%s", Application::GetTraits().supportsUri ? helpUrl.c_str() : ""); + printf(" + execute an ingame command after startup\n" "\n" "Order is important, -options must be set before +commands.\n" "Nothing is read and executed after -connect option and the following URI.\n" @@ -748,7 +747,7 @@ static void ParseCmdline(int argc, char** argv, cmdlineArgs_t& cmdlineArgs) FS::FlushAll(); OSExit(0); } else if (!strcmp(argv[i], "--version") || !strcmp(argv[i], "-version") || !strcmp(argv[i], "-v")) { - printf(PRODUCT_NAME " " PRODUCT_VERSION "\n"); + printf(ENGINE_NAME_VERSION "\n"); FS::FlushAll(); OSExit(0); } else if (!strcmp(argv[i], "-set")) { @@ -836,17 +835,20 @@ static void Init(int argc, char** argv) // Detect MSYS2 terminal. The AttachConsole code makes output not appear const char* msystem = getenv("MSYSTEM"); if (!msystem || !Str::IsPrefix("MINGW", msystem)) { - // If we were launched from a console, make our output visible on it - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - (void)freopen("CONOUT$", "w", stdout); - (void)freopen("CONOUT$", "w", stderr); - } + // If we were launched from a console, make our output visible on it + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + (void)freopen("CONOUT$", "w", stdout); + (void)freopen("CONOUT$", "w", stderr); + } } #endif - // Print a banner and a copy of the command-line arguments - Log::Notice("%s %s %s (%s) %s", Q3_VERSION, PLATFORM_STRING, DAEMON_ARCH_STRING, DAEMON_CXX_COMPILER_STRING, __DATE__); + Sys::ParseCmdline(argc, argv, cmdlineArgs); + GameInfo::getInstance().parse(FS::Path::Build(cmdlineArgs.libPath, GameInfo::fileName)); + Log::Notice("%s", PRODUCT_NAME_VERSION); + Log::Notice("%s %s %s (%s) %s", ENGINE_NAME_VERSION, PLATFORM_STRING, DAEMON_ARCH_STRING, DAEMON_CXX_COMPILER_STRING, ENGINE_DATE); + // Print a banner and a copy of the command-line arguments std::string argsString = "cmdline:"; for (int i = 1; i < argc; i++) { argsString.push_back(' '); @@ -854,7 +856,9 @@ static void Init(int argc, char** argv) } Log::Notice(argsString); - Sys::ParseCmdline(argc, argv, cmdlineArgs); + if (cmdlineArgs.homePath.empty()) { + cmdlineArgs.homePath = FS::DefaultHomePath(); + } if (cmdlineArgs.use_crash_handlers) { Sys::SetupCrashHandler(); // If Breakpad is enabled, this handler will soon be replaced. @@ -915,11 +919,11 @@ static void Init(int argc, char** argv) if (ConnectSingletonSocket()) { Log::Notice("Existing instance found"); if (cmdlineArgs.allowForwardToExistingInstance) { - if (!cmdlineArgs.commands.empty()) { - Log::Notice("Forwarding commands to existing instance"); - WriteSingletonSocket(cmdlineArgs.commands); + if (!cmdlineArgs.commands.empty()) { + Log::Notice("Forwarding commands to existing instance"); + WriteSingletonSocket(cmdlineArgs.commands); } else { - Log::Notice("No commands given, exiting..."); + Log::Notice("No commands given, exiting..."); } } #ifdef _WIN32 @@ -946,8 +950,8 @@ static void Init(int argc, char** argv) if (CreateCrashDumpPath() && cmdlineArgs.use_crash_handlers) { // This may fork(), and then exec() *in the parent process*, // so threads must not be created before this point. - BreakpadInit(); - } + BreakpadInit(); + } // Start a thread which reads commands from the singleton socket try { diff --git a/src/engine/qcommon/common.cpp b/src/engine/qcommon/common.cpp index d96e7ea368..4d84602552 100644 --- a/src/engine/qcommon/common.cpp +++ b/src/engine/qcommon/common.cpp @@ -571,7 +571,7 @@ void Com_Init() Cmd_AddCommand( "writebindings", Com_WriteBindings_f ); #endif - s = va( "%s %s %s %s", Q3_VERSION, PLATFORM_STRING, DAEMON_ARCH_STRING, __DATE__ ); + s = va( "%s %s %s %s %s", PRODUCT_NAME_VERSION, ENGINE_NAME_VERSION, PLATFORM_STRING, DAEMON_ARCH_STRING, __DATE__ ); com_version = Cvar_Get( "version", s, CVAR_ROM | CVAR_SERVERINFO | CVAR_USERINFO ); Cmd_AddCommand( "in_restart", Com_In_Restart_f ); @@ -615,7 +615,7 @@ void Com_WriteConfigToFile( const char *filename, void (*writeConfig)( fileHandl return; } - FS_Printf( f, "// generated by " PRODUCT_NAME ", do not modify\n" ); + FS_Printf( f, "// generated by %s, do not modify\n", GameInfo::getInstance().name().c_str() ); writeConfig( f ); FS_FCloseFile( f ); } diff --git a/src/engine/qcommon/files.cpp b/src/engine/qcommon/files.cpp index e2b97a861e..c38254e9ef 100644 --- a/src/engine/qcommon/files.cpp +++ b/src/engine/qcommon/files.cpp @@ -36,7 +36,7 @@ constexpr FS::offset_t MAX_FILE_LENGTH = 1000 * 1000 * 1000; const char TEMP_SUFFIX[] = ".tmp"; // Cvars to select the base and extra packages to use -static Cvar::Cvar fs_basepak("fs_basepak", "base pak to load", 0, DEFAULT_BASE_PAK); +static Cvar::Cvar fs_basepak( "fs_basepak", "base pak to load", 0, "" ); static Cvar::Cvar fs_extrapaks("fs_extrapaks", "space-seperated list of paks to load in addition to the base pak", 0, ""); struct handleData_t { @@ -667,14 +667,18 @@ void FS_LoadBasePak() return; // success } - if (fs_basepak.Get() != DEFAULT_BASE_PAK) { - Log::Notice("Could not load base pak '%s', falling back to default: '%s'", fs_basepak.Get().c_str(), DEFAULT_BASE_PAK); - if (FS_LoadPak(DEFAULT_BASE_PAK)) { + std::string defaultBasePak = GameInfo::getInstance().basePak(); + + if (fs_basepak.Get() != defaultBasePak) { + Log::Notice("Could not load base pak '%s', falling back to default: '%s'", + fs_basepak.Get(), defaultBasePak); + + if (FS_LoadPak(defaultBasePak)) { return; // success } } - Sys::Error("Could not load default base pak '%s'", DEFAULT_BASE_PAK); + Sys::Error("Could not load default base pak '%s'", defaultBasePak); } bool FS_LoadServerPaks(const char* paks, bool isDemo) diff --git a/src/engine/qcommon/q_shared.h b/src/engine/qcommon/q_shared.h index 7b33e27f36..7b303909bb 100644 --- a/src/engine/qcommon/q_shared.h +++ b/src/engine/qcommon/q_shared.h @@ -36,6 +36,7 @@ Maryland 20850 USA. #define Q_SHARED_H_ #include "common/Defs.h" +#include "engine/framework/GameInfo.h" #if defined(BUILD_VM) #include "DaemonBuildInfo/Game.h" @@ -50,21 +51,15 @@ Maryland 20850 USA. #define _USE_MATH_DEFINES #endif - // q_shared.h -- included first by ALL program modules. // A user mod should never modify this file -#define ENGINE_NAME "Daemon Engine" -#define ENGINE_VERSION PRODUCT_VERSION - -#define Q3_VERSION PRODUCT_NAME " " PRODUCT_VERSION +#define PRODUCT_NAME_VERSION va("%s %s", GameInfo::getInstance().name().c_str(), GameInfo::getInstance().version().c_str()) -#define Q3_ENGINE ENGINE_NAME " " ENGINE_VERSION -#define Q3_ENGINE_DATE __DATE__ - -#define CLIENT_WINDOW_TITLE PRODUCT_NAME -#define CLIENT_WINDOW_MIN_TITLE PRODUCT_NAME_LOWER +#define ENGINE_NAME_VERSION ENGINE_NAME " " ENGINE_VERSION +#define CLIENT_WINDOW_TITLE GameInfo::getInstance().name().c_str() +#define CLIENT_WINDOW_MIN_TITLE GameInfo::getInstance().name_lower().c_str() #define Q_UNUSED(x) (void)(x) @@ -347,19 +342,19 @@ extern const quat_t quatIdentity; /* The original Q_rsqrt algorithm is: float Q_rsqrt( float n ) -{ + { uint32_t magic = 0x5f3759dful; float a = 0.5f; float b = 3.0f; union { float f; uint32_t u; } o = {n}; o.u = magic - ( o.u >> 1 ); return a * o.f * ( b - n * o.f * o.f ); -} + } It could be written like this, this is what Quake 3 did: float Q_rsqrt( float n ) -{ + { uint32_t magic = 0x5f3759dful; float a = 0.5f; float b = 3.0f; @@ -371,7 +366,7 @@ float Q_rsqrt( float n ) o.f *= c - ( x * o.f * o.f ); // o.f *= c - ( x * o.f * o.f ); return o.f; -} + } It was written with a second iteration commented out. @@ -391,7 +386,7 @@ Jan Kadlec computed an ever better magic constant but it requires different values for the first iteration: http://rrrola.wz.cz/inv_sqrt.html float Q_rsqrt( float n ) -{ + { uint32_t magic = 0x5f1ffff9ul: float a = 0.703952253f; float b = 2.38924456f; @@ -420,7 +415,7 @@ WARN_UNUSED_RESULT inline float Q_rsqrt_fast( const float n ) o *= a * ( b - n * o * o ); #endif return o; -} + } WARN_UNUSED_RESULT inline float Q_rsqrt( const float n ) { @@ -1817,7 +1812,7 @@ inline vec_t VectorNormalize2( const vec3_t v, vec3_t out ) // if it does not want binds to execute. The only thing that really depends on this flag is // BUTTON_TALK / BUTTON_ANY. But BUTTON_TALK could simply be determined by the cgame, and // BUTTON_ANY does not seem useful at all. -#define KEYCATCH_CONSOLE 0x0001 +#define KEYCATCH_CONSOLE 0x0001 #define KEYCATCH_UI_KEY 0x0002 #define KEYCATCH_UI_MOUSE 0x0004 diff --git a/src/engine/qcommon/qcommon.h b/src/engine/qcommon/qcommon.h index c3d56f5157..67af6fc4fa 100644 --- a/src/engine/qcommon/qcommon.h +++ b/src/engine/qcommon/qcommon.h @@ -41,6 +41,10 @@ Maryland 20850 USA. #include "common/Defs.h" #include "net_types.h" +#if !defined(BUILD_VM) +#include "framework/GameInfo.h" +#endif + //============================================================================ // @@ -231,9 +235,6 @@ You or the server may be running older versions of the game." #define PROTOCOL_VERSION 86 -#define URI_SCHEME GAMENAME_STRING "://" -#define URI_SCHEME_LENGTH ( ARRAY_LEN( URI_SCHEME ) - 1 ) - #define PORT_MASTER 27950 #define PORT_SERVER 27960 #define NUM_SERVER_PORTS 4 // broadcast scan this many ports after diff --git a/src/engine/renderer/tr_init.cpp b/src/engine/renderer/tr_init.cpp index b6174545d7..c0cb5a6db8 100644 --- a/src/engine/renderer/tr_init.cpp +++ b/src/engine/renderer/tr_init.cpp @@ -731,9 +731,12 @@ class ScreenshotCmd : public Cmd::StaticCmd { } std::string fileName; + std::string baseName = GameInfo::getInstance().baseName(); + if ( args.Argc() == 2 ) { - fileName = Str::Format( "screenshots/" PRODUCT_NAME_LOWER "-%s.%s", args.Argv(1), fileExtension ); + fileName = Str::Format("screenshots/%s-%s.%s", baseName, + args.Argv(1), fileExtension ); } else { @@ -744,8 +747,9 @@ class ScreenshotCmd : public Cmd::StaticCmd { int lastNumber; for ( lastNumber = 0; lastNumber <= 999; lastNumber++ ) { - fileName = Str::Format( "screenshots/" PRODUCT_NAME_LOWER "_%04d-%02d-%02d_%02d%02d%02d_%03d.%s", - 1900 + t.tm_year, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, lastNumber, fileExtension ); + fileName = Str::Format( "screenshots/%s_%04d-%02d-%02d_%02d%02d%02d_%03d.%s", + baseName, 1900 + t.tm_year, t.tm_mon + 1, + t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, lastNumber, fileExtension ); if ( !FS::HomePath::FileExists( fileName.c_str() ) ) { diff --git a/src/engine/server/sv_ccmds.cpp b/src/engine/server/sv_ccmds.cpp index 5f066e1fd1..b31313c57a 100644 --- a/src/engine/server/sv_ccmds.cpp +++ b/src/engine/server/sv_ccmds.cpp @@ -284,7 +284,8 @@ class StatusCmd: public Cmd::StaticCmd Print( "(begin server status)\n" "hostname: %s\n" - "version: %s\n" + "engine: %s\n" + "game: %s\n" "protocol: %d\n" "cpu: %.0f%%\n" "time: %s\n" @@ -293,7 +294,8 @@ class StatusCmd: public Cmd::StaticCmd "num score connection address port name\n" "--- ----- ---------- ---------------------- ------ ----", sv_hostname.Get(), - Q3_VERSION " on " Q3_ENGINE, + ENGINE_NAME_VERSION, + PRODUCT_NAME_VERSION, PROTOCOL_VERSION, cpu, time_string, diff --git a/src/engine/server/sv_client.cpp b/src/engine/server/sv_client.cpp index 3b6f336754..b0234e3cd6 100644 --- a/src/engine/server/sv_client.cpp +++ b/src/engine/server/sv_client.cpp @@ -42,7 +42,7 @@ Maryland 20850 USA. // HTTP download params static Cvar::Cvar sv_wwwDownload("sv_wwwDownload", "have clients download missing paks via HTTP", Cvar::NONE, true); -static Cvar::Cvar sv_wwwBaseURL("sv_wwwBaseURL", "where clients download paks (must NOT be HTTPS, must contain PAKSERVER)", Cvar::NONE, WWW_BASEURL); +static Cvar::Cvar sv_wwwBaseURL("sv_wwwBaseURL", "where clients download paks (must NOT be HTTPS, must contain PAKSERVER)", Cvar::NONE, GameInfo::getInstance().wwwBaseUrl()); static Cvar::Cvar sv_wwwFallbackURL("sv_wwwFallbackURL", "alternative download site to sv_wwwBaseURL", Cvar::NONE, ""); static void SV_CloseDownload( client_t *cl ); diff --git a/src/engine/server/sv_init.cpp b/src/engine/server/sv_init.cpp index 14315f1ad1..04eac0136b 100644 --- a/src/engine/server/sv_init.cpp +++ b/src/engine/server/sv_init.cpp @@ -465,8 +465,8 @@ void SV_SpawnServer(std::string pakname, std::string mapname) else { // check for maxclients change - SV_ChangeMaxClients(); - } + SV_ChangeMaxClients(); + } // allocate the snapshot entities svs.snapshotEntities.reset(new entityState_t[svs.numSnapshotEntities]); diff --git a/src/engine/server/sv_main.cpp b/src/engine/server/sv_main.cpp index 293acb8c8f..ad062d8d7b 100644 --- a/src/engine/server/sv_main.cpp +++ b/src/engine/server/sv_main.cpp @@ -67,7 +67,7 @@ Cvar::Range> sv_maxClients("sv_maxclients", Cvar::Range> sv_privateClients("sv_privateClients", "number of password-protected client slots", Cvar::SERVERINFO, 0, 0, MAX_CLIENTS); -Cvar::Cvar sv_hostname("sv_hostname", "server name for server list", Cvar::SERVERINFO, UNNAMED_SERVER); +Cvar::Cvar sv_hostname("sv_hostname", "server name for server list", Cvar::SERVERINFO, ""); Cvar::Cvar sv_statsURL("sv_statsURL", "URL for server's gameplay statistics", Cvar::SERVERINFO, ""); Cvar::Cvar sv_reconnectlimit("sv_reconnectlimit", "minimum time (seconds) before client can reconnect", Cvar::NONE, 3); Cvar::Cvar sv_padPackets("sv_padPackets", "(debugging) add n NOP bytes to each snapshot packet", Cvar::NONE, 0); @@ -281,17 +281,17 @@ struct MasterServer netadrtype_t challenge_address_type; Cvar::Modified> addressCvar; - MasterServer(const char* cvarName, const char* defaultHostname) - : addressCvar(cvarName, "address of a master server", Cvar::NONE, defaultHostname ) + MasterServer(const char* cvarName) + : addressCvar(cvarName, "address of a master server", Cvar::NONE, "") {} }; MasterServer masterServers[MAX_MASTER_SERVERS] = { - {"sv_master1", MASTER1_SERVER_NAME}, - {"sv_master2", MASTER2_SERVER_NAME}, - {"sv_master3", MASTER3_SERVER_NAME}, - {"sv_master4", MASTER4_SERVER_NAME}, - {"sv_master5", MASTER5_SERVER_NAME}, + {"sv_master1"}, + {"sv_master2"}, + {"sv_master3"}, + {"sv_master4"}, + {"sv_master5"}, }; } // namespace @@ -359,8 +359,8 @@ but not on every player enter or exit. ================ */ static const int HEARTBEAT_MSEC = (300 * 1000); -#define HEARTBEAT_GAME PRODUCT_NAME -#define HEARTBEAT_DEAD PRODUCT_NAME "-dead" +#define HEARTBEAT_GAME ENGINE_NAME +#define HEARTBEAT_DEAD HEARTBEAT_GAME "-dead" void SV_MasterHeartbeat( const char *hbname ) { @@ -595,7 +595,7 @@ static void SVC_Info( const netadr_t& from, const Cmd::Args& args ) info_map["stats"] = sv_statsURL.Get().c_str(); } - info_map["gamename"] = GAMENAME_STRING; // Arnout: to be able to filter out Quake servers + info_map["gamename"] = GameInfo::getInstance().masterGameName(); // Arnout: to be able to filter out Quake servers info_map["abi"] = IPC::SYSCALL_ABI_VERSION; // Add the engine version. But is that really what we want? Probably the gamelogic version would // be more interesting to players. Oh well, it's what's available for now. @@ -1289,11 +1289,11 @@ int SV_FrameMsec() int scaledResidual = static_cast( sv.timeResidual / com_timescale->value ); if ( frameMsec < scaledResidual ) - { - return 0; - } - else - { + { + return 0; + } + else + { return frameMsec - scaledResidual; } } diff --git a/src/engine/sys/con_curses.cpp b/src/engine/sys/con_curses.cpp index add5c81457..19527af0fb 100644 --- a/src/engine/sys/con_curses.cpp +++ b/src/engine/sys/con_curses.cpp @@ -35,9 +35,9 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include #ifdef BUILD_SERVER -#define TITLE "^2[ ^3" CLIENT_WINDOW_TITLE " Server Console ^2]" +#define TITLE va("^2[ ^3%s Server Console ^2]", CLIENT_WINDOW_TITLE) #else -#define TITLE "^2[ ^3" CLIENT_WINDOW_TITLE " Console ^2]" +#define TITLE va("^2[ ^3%s Console ^2]", CLIENT_WINDOW_TITLE) #endif #define PROMPT "^3-> " static const int LOG_SCROLL = 5; diff --git a/src/engine/sys/sdl_glimp.cpp b/src/engine/sys/sdl_glimp.cpp index c15add9f0e..33bde8f8e4 100644 --- a/src/engine/sys/sdl_glimp.cpp +++ b/src/engine/sys/sdl_glimp.cpp @@ -1732,15 +1732,18 @@ GLimp_StartDriverAndSetMode static rserr_t GLimp_StartDriverAndSetMode( int mode, bool fullscreen, bool bordered ) { // See the SDL wiki page for details: https://wiki.libsdl.org/SDL3/SDL_SetAppMetadataProperty - SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_NAME_STRING, PRODUCT_NAME ); - SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_VERSION_STRING, PRODUCT_VERSION ); + SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_NAME_STRING, + GameInfo::getInstance().name().c_str() ); + SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_VERSION_STRING, + GameInfo::getInstance().version().c_str() ); SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_TYPE_STRING, "game" ); /* Let X11 and Wayland desktops (Linux, FreeBSD…) associate the game window with the XDG .desktop file, with the proper name and icon. The .desktop file should have PRODUCT_APPID as base name or set the StartupWMClass variable to PRODUCT_APPID. */ - SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_IDENTIFIER_STRING, PRODUCT_APPID ); + SDL_SetAppMetadataProperty( SDL_PROP_APP_METADATA_IDENTIFIER_STRING, + GameInfo::getInstance().appId().c_str() ); /* Disable DPI scaling. See the SDL wiki page for details: https://wiki.libsdl.org/SDL3/SDL_HINT_VIDEO_WAYLAND_SCALE_TO_DISPLAY */ diff --git a/srclibs.cmake b/srclibs.cmake index 08b1a31b7a..eeae80668e 100644 --- a/srclibs.cmake +++ b/srclibs.cmake @@ -1,3 +1,10 @@ +set(TOMLC17LIST + ${LIB_DIR}/tomlc17/tomlc17.c + ${LIB_DIR}/tomlc17/tomlc17.h +# tomlcpp.hpp requires C++20. +# ${LIB_DIR}/tomlc17/tomlcpp.hpp +) + set(CRUNCHLIST ${LIB_DIR}/crunch/inc/crn_decomp.h ${LIB_DIR}/crunch/inc/crnlib.h