Files
RetroArch/libretro-db/query.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

1311 lines
35 KiB
C

/* Copyright (C) 2010-2017 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (query.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifdef _WIN32
#include <direct.h>
#else
#include <unistd.h>
#endif
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <compat/fnmatch.h>
#include <compat/strl.h>
#include <string/stdstring.h>
#include <retro_miscellaneous.h>
#include "libretrodb.h"
#include "query.h"
#include "rmsgpack_dom.h"
#define MAX_ERROR_LEN 256
#define QUERY_MAX_ARGS 50
struct buffer
{
const char *data;
size_t len;
ssize_t offset;
};
enum argument_type
{
AT_FUNCTION = 0,
AT_VALUE
};
struct argument;
/* Per-query evaluation state.
*
* query_func_min()/query_func_max() report "smaller/larger than
* everything seen so far", so they need somewhere to keep the running
* extreme across the rows of a walk. That used to be a single
* file-scope 'intermediate_res', which two queries evaluated
* concurrently would corrupt for each other:
*
* WARNING: ThreadSanitizer: data race
* Write of size 8 by thread T2:
* #1 query_func_min query.c:310
* Location is global 'intermediate_res' of size 24
*
* Give every compiled query its own, and pass it to the evaluation
* functions rather than reaching for a global. */
struct query_ctx
{
struct rmsgpack_dom_value intermediate_res;
/* Whether intermediate_res holds a value yet. min() and max() used
* zero for that, which is indistinguishable from a stored zero: a
* record with size 0 read as "nothing accumulated", the running
* extreme was discarded, and the walk started again from the next
* record. The reported extreme was then taken over the tail of the
* database rather than all of it.
*
* Live in the databases shipped today: Mobile - J2ME carries one
* zero-size record two thirds of the way in, so max(size) came back
* as 25639896 against a true 27853509, and the two largest entries
* sat outside the range the scanner filters on - they could not be
* matched at all. */
int have_res;
};
typedef struct rmsgpack_dom_value (*rarch_query_func)(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument *argv);
struct invocation
{
struct argument *argv;
rarch_query_func func;
unsigned argc;
};
struct argument
{
union
{
struct rmsgpack_dom_value value;
struct invocation invocation;
} a;
enum argument_type type;
};
struct query
{
struct invocation root; /* ptr alignment */
struct query_ctx ctx;
unsigned ref_count;
};
struct registered_func
{
const char *name;
rarch_query_func func;
};
void libretrodb_query_reset_accumulator(libretrodb_query_t *q)
{
struct query *rq = (struct query *)q;
if (!rq)
return;
rq->ctx.intermediate_res.val.int_ = 0;
rq->ctx.intermediate_res.val.uint_ = 0;
rq->ctx.have_res = 0;
}
/* Forward declarations */
static struct buffer query_parse_method_call(char *s, size_t len,
struct buffer buff, struct invocation *invocation, const char **err);
static struct buffer query_parse_table(char *s, size_t len, struct buffer buff,
struct invocation *invocation, const char **err);
/* Errors */
static struct rmsgpack_dom_value query_func_is_true(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument *argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
if (!(argc > 0 || input.type != RDT_BOOL))
res.val.bool_ = input.val.bool_;
return res;
}
static struct rmsgpack_dom_value func_equals(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
if (argc == 1)
{
struct argument arg = argv[0];
if (arg.type == AT_VALUE)
{
if ( input.type == RDT_UINT
&& arg.a.value.type == RDT_INT)
{
arg.a.value.type = RDT_UINT;
arg.a.value.val.uint_ = arg.a.value.val.int_;
}
res.val.bool_ = (rmsgpack_dom_value_cmp(&input, &arg.a.value) == 0);
}
}
return res;
}
static struct rmsgpack_dom_value query_func_operator_or(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
size_t i;
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
for (i = 0; i < argc; i++)
{
if (argv[i].type == AT_VALUE)
res = func_equals(ctx, input, 1, &argv[i]);
else
res = query_func_is_true(ctx,
argv[i].a.invocation.func(ctx, input,
argv[i].a.invocation.argc,
argv[i].a.invocation.argv
), 0, NULL);
if (res.val.bool_)
break;
}
return res;
}
static struct rmsgpack_dom_value query_func_operator_and(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
size_t i;
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
for (i = 0; i < argc; i++)
{
if (argv[i].type == AT_VALUE)
res = func_equals(ctx, input, 1, &argv[i]);
else
res = query_func_is_true(ctx,
argv[i].a.invocation.func(ctx, input,
argv[i].a.invocation.argc,
argv[i].a.invocation.argv
),
0, NULL);
if (!res.val.bool_)
break;
}
return res;
}
static struct rmsgpack_dom_value query_func_between(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
if (argc != 2)
return res;
if ( argv[0].type != AT_VALUE
|| argv[1].type != AT_VALUE)
return res;
if ( argv[0].a.value.type != RDT_INT
|| argv[1].a.value.type != RDT_INT)
return res;
switch (input.type)
{
case RDT_INT:
res.val.bool_ = (
(input.val.int_ >= argv[0].a.value.val.int_)
&& (input.val.int_ <= argv[1].a.value.val.int_));
break;
case RDT_UINT:
res.val.bool_ = (
((unsigned)input.val.int_ >= argv[0].a.value.val.uint_)
&& (input.val.int_ <= argv[1].a.value.val.int_));
break;
default:
break;
}
return res;
}
static struct rmsgpack_dom_value query_func_glob(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
if (argc != 1)
return res;
if (argv[0].type != AT_VALUE || argv[0].a.value.type != RDT_STRING)
return res;
if (input.type == RDT_STRING)
res.val.bool_ = rl_fnmatch(
argv[0].a.value.val.string.buff,
input.val.string.buff,
0
) == 0;
return res;
}
/* Min and max functions will return all entries smaller/larger than previous *
* - in practice, last entry will contain the actual min/max value. *
* Empty result means there is no such field. */
static struct rmsgpack_dom_value query_func_min(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
switch (input.type)
{
case RDT_INT:
res.val.bool_ = (!ctx->have_res
|| (input.val.int_ < ctx->intermediate_res.val.int_));
if (res.val.bool_)
{
memcpy(&ctx->intermediate_res, &input, sizeof(ctx->intermediate_res));
ctx->have_res = 1;
}
break;
case RDT_UINT:
res.val.bool_ = (!ctx->have_res
|| (input.val.uint_ < ctx->intermediate_res.val.uint_));
if (res.val.bool_)
{
memcpy(&ctx->intermediate_res, &input, sizeof(ctx->intermediate_res));
ctx->have_res = 1;
}
break;
default:
break;
}
return res;
}
static struct rmsgpack_dom_value query_func_max(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument * argv)
{
struct rmsgpack_dom_value res;
res.type = RDT_BOOL;
res.val.bool_ = 0;
switch (input.type)
{
case RDT_INT:
res.val.bool_ = (!ctx->have_res
|| (input.val.int_ > ctx->intermediate_res.val.int_));
if (res.val.bool_)
{
memcpy(&ctx->intermediate_res, &input, sizeof(ctx->intermediate_res));
ctx->have_res = 1;
}
break;
case RDT_UINT:
res.val.bool_ = (!ctx->have_res
|| (input.val.uint_ > ctx->intermediate_res.val.uint_));
if (res.val.bool_)
{
memcpy(&ctx->intermediate_res, &input, sizeof(ctx->intermediate_res));
ctx->have_res = 1;
}
break;
default:
break;
}
return res;
}
struct registered_func registered_functions[100] = {
{"is_true", query_func_is_true},
{"or", query_func_operator_or},
{"and", query_func_operator_and},
{"between", query_func_between},
{"glob", query_func_glob},
{"min", query_func_min},
{"max", query_func_max},
{NULL, NULL}
};
static void query_raise_unknown_function(
char *s, size_t len,
ssize_t where, const char *name,
ssize_t name_len, const char **err)
{
size_t off = snprintf(s, len,
"%" PRIu64 "::Unknown function '",
(uint64_t)where);
if (off < len)
{
size_t remaining = len - off;
size_t copy_len = (size_t)name_len < remaining ? (size_t)name_len : remaining - 1;
/* See query_parse_table(): 'name' is a slice of the query, not
* a C string, so it cannot be handed to strlcpy(). */
memcpy(s + off, name, copy_len);
off += copy_len;
s[off] = '\0';
}
if (off + 2 <= len)
{
s[off] = '\'';
s[off + 1] = '\0';
}
else if (len > 0)
s[len - 1] = '\0';
*err = s;
}
static void query_argument_free(struct argument *arg)
{
size_t i;
if (arg->type != AT_FUNCTION)
{
rmsgpack_dom_value_free(&arg->a.value);
return;
}
for (i = 0; i < arg->a.invocation.argc; i++)
query_argument_free(&arg->a.invocation.argv[i]);
free((void*)arg->a.invocation.argv);
}
static struct buffer query_parse_integer(
char *s, size_t len,
struct buffer buff,
struct rmsgpack_dom_value *value,
const char **err)
{
int64_t result = 0;
int sign = 1;
bool has_digit = false;
bool overflowed = false;
size_t idx = buff.offset;
value->type = RDT_INT;
if (idx < buff.len && buff.data[idx] == '-')
{
sign = -1;
idx++;
}
else if (idx < buff.len && buff.data[idx] == '+')
idx++;
while (idx < buff.len && ISDIGIT((int)buff.data[idx]))
{
int digit = buff.data[idx] - '0';
has_digit = true;
/* Signed overflow is undefined, and the digit count here comes
* from the query, so bound the accumulation rather than letting
* it wrap. */
if ( result > (INT64_MAX - digit) / 10
|| overflowed)
overflowed = true;
else
result = result * 10 + digit;
idx++;
}
if (overflowed)
{
snprintf(s, len,
"%" PRIu64 "::Number out of range",
(uint64_t)buff.offset);
*err = s;
return buff;
}
if (!has_digit)
{
snprintf(s, len,
"%" PRIu64 "::Expected number",
(uint64_t)buff.offset);
*err = s;
}
else
{
value->val.int_ = result * sign;
buff.offset = idx;
}
return buff;
}
static struct buffer query_chomp(struct buffer buff)
{
for (; (unsigned)buff.offset < buff.len
&& ISSPACE((int)buff.data[buff.offset]); buff.offset++);
return buff;
}
static struct buffer query_expect_eof(char *s, size_t len,
struct buffer buff, const char ** err)
{
buff = query_chomp(buff);
if ((unsigned)buff.offset < buff.len)
{
snprintf(s, len,
"%" PRIu64 "::Expected EOF found '%c'",
(uint64_t)buff.offset,
buff.data[buff.offset]
);
*err = s;
}
return buff;
}
static int query_peek(struct buffer buff, const char * data,
size_t len)
{
size_t remain = buff.len - buff.offset;
if (remain < len)
return 0;
return (strncmp(buff.data + buff.offset, data, len) == 0);
}
static struct buffer query_get_char(
char *s, size_t len,
struct buffer buff, char * c,
const char ** err)
{
if ((unsigned)buff.offset >= buff.len)
{
snprintf(s, len,
"%" PRIu64 "::Unexpected EOF",
(uint64_t)buff.offset
);
*err = s;
return buff;
}
*c = buff.data[buff.offset];
buff.offset++;
return buff;
}
static struct buffer query_parse_string(
char *s, size_t len,
struct buffer buff,
struct rmsgpack_dom_value *value, const char **err)
{
const char * str_start = NULL;
char terminator = '\0';
char c = '\0';
int is_binstr = 0;
buff = query_get_char(s, len,buff, &terminator, err);
if (*err)
return buff;
if (terminator == 'b')
{
is_binstr = 1;
buff = query_get_char(s, len, buff, &terminator, err);
}
if (terminator != '"' && terminator != '\'')
{
buff.offset--;
snprintf(s, len,
"%" PRIu64 "::Expected string",
(uint64_t)buff.offset);
*err = s;
}
str_start = buff.data + buff.offset;
buff = query_get_char(s, len, buff, &c, err);
while (!*err)
{
if (c == terminator)
break;
buff = query_get_char(s, len, buff, &c, err);
}
if (!*err)
{
size_t count;
value->type = is_binstr ? RDT_BINARY : RDT_STRING;
value->val.string.len = (uint32_t)((buff.data + buff.offset) - str_start - 1);
count = value->val.string.len + 1;
if (is_binstr)
count /= 2;
/* b"" sizes this to zero, and calloc(0) may return NULL, which
* the check below would report as an allocation failure. */
if (count == 0)
count = 1;
value->val.string.buff = (char*)calloc(count, sizeof(char));
if (!value->val.string.buff)
{
s[0] = 'O';
s[1] = 'O';
s[2] = 'M';
s[3] = '\0';
*err = s;
}
else if (is_binstr)
{
size_t i;
int j = 0;
const char *tok = str_start;
for (i = 0; i < value->val.string.len; i += 2)
{
uint8_t hi, lo;
char hic = tok[i];
char loc = tok[i + 1];
if (hic <= '9')
hi = hic - '0';
else
hi = (hic - 'A') + 10;
if (loc <= '9')
lo = loc - '0';
else
lo = (loc - 'A') + 10;
value->val.string.buff[j++] = hi * 16 + lo;
}
value->val.string.len = j;
}
else
memcpy(value->val.string.buff, str_start, value->val.string.len);
}
return buff;
}
static struct buffer query_parse_value(char *s, size_t len,
struct buffer buff, struct rmsgpack_dom_value *value,
const char **err)
{
buff = query_chomp(buff);
if (query_peek(buff, "nil", STRLEN_CONST("nil")))
{
buff.offset += STRLEN_CONST("nil");
value->type = RDT_NULL;
}
else if (query_peek(buff, "true", STRLEN_CONST("true")))
{
buff.offset += STRLEN_CONST("true");
value->type = RDT_BOOL;
value->val.bool_ = 1;
}
else if (query_peek(buff, "false", STRLEN_CONST("false")))
{
buff.offset += STRLEN_CONST("false");
value->type = RDT_BOOL;
value->val.bool_ = 0;
}
else if (
query_peek(buff, "b", STRLEN_CONST("b"))
|| query_peek(buff, "\"", STRLEN_CONST("\""))
|| query_peek(buff, "'", STRLEN_CONST("'")))
buff = query_parse_string(s, len, buff, value, err);
else if ( (size_t)buff.offset < buff.len
&& ISDIGIT((int)buff.data[buff.offset]))
buff = query_parse_integer(s, len, buff, value, err);
return buff;
}
static void query_peek_char(char *s, size_t len,
struct buffer buff, char *c, const char **err)
{
if ((unsigned)buff.offset >= buff.len)
{
snprintf(s, len,
"%" PRIu64 "::Unexpected EOF",
(uint64_t)buff.offset
);
*err = s;
return;
}
*c = buff.data[buff.offset];
}
static struct buffer query_get_ident(char *s, size_t _len,
struct buffer buff, const char **ident,
size_t *len, const char **err)
{
char c = '\0';
if ((unsigned)buff.offset >= buff.len)
{
snprintf(s, _len,
"%" PRIu64 "::Unexpected EOF",
(uint64_t)buff.offset
);
*err = s;
return buff;
}
*ident = buff.data + buff.offset;
*len = 0;
query_peek_char(s, _len, buff, &c, err);
if (*err || !ISALPHA((int)c))
return buff;
buff.offset++;
*len = *len + 1;
query_peek_char(s, _len, buff, &c, err);
while (!*err)
{
if (!(ISALNUM((int)c) || c == '_'))
break;
buff.offset++;
*len = *len + 1;
query_peek_char(s, _len, buff, &c, err);
}
return buff;
}
static struct buffer query_expect_char(
char *s, size_t len,
struct buffer buff,
char c, const char ** err)
{
if ((unsigned)buff.offset >= buff.len)
{
snprintf(s, len,
"%" PRIu64 "::Unexpected EOF",
(uint64_t)buff.offset
);
*err = s;
}
else if (buff.data[buff.offset] != c)
{
snprintf(s, len,
"%" PRIu64 "::Expected '%c' found '%c'",
(uint64_t)buff.offset, c, buff.data[buff.offset]);
*err = s;
}
else
buff.offset++;
return buff;
}
static struct buffer query_parse_argument(
char *s, size_t len,
struct buffer buff,
struct argument *arg, const char **err)
{
buff = query_chomp(buff);
if (
(size_t)buff.offset < buff.len
&& ISALPHA((int)buff.data[buff.offset])
&& !(
query_peek(buff, "nil", STRLEN_CONST("nil"))
|| query_peek(buff, "true", STRLEN_CONST("true"))
|| query_peek(buff, "false", STRLEN_CONST("false"))
|| query_peek(buff, "b\"", STRLEN_CONST("b\""))
|| query_peek(buff, "b'", STRLEN_CONST("b'"))
/* bin string prefix*/
)
)
{
arg->type = AT_FUNCTION;
buff = query_parse_method_call(s, len, buff,
&arg->a.invocation, err);
}
else if (query_peek(buff, "{", STRLEN_CONST("{")))
{
arg->type = AT_FUNCTION;
buff = query_parse_table(s, len,
buff, &arg->a.invocation, err);
}
else
{
arg->type = AT_VALUE;
buff = query_parse_value(s,
len, buff, &arg->a.value, err);
}
return buff;
}
static struct rmsgpack_dom_value query_func_all_map(
struct query_ctx *ctx,
struct rmsgpack_dom_value input,
unsigned argc, const struct argument *argv)
{
struct rmsgpack_dom_value res;
struct rmsgpack_dom_value nil_value;
struct rmsgpack_dom_value *value = NULL;
res.type = RDT_BOOL;
res.val.bool_ = 1;
nil_value.type = RDT_NULL;
if (argc % 2 != 0)
{
res.val.bool_ = 0;
return res;
}
if (input.type == RDT_MAP)
{
unsigned i;
for (i = 0; i < argc; i += 2)
{
struct argument arg = argv[i];
if (arg.type != AT_VALUE)
{
res.val.bool_ = 0;
return res;
}
/* All missing fields are nil */
if (!(value = rmsgpack_dom_value_map_value(&input, &arg.a.value)))
value = &nil_value;
arg = argv[i + 1];
if (arg.type == AT_VALUE)
res = func_equals(ctx, *value, 1, &arg);
else
{
res = query_func_is_true(ctx, arg.a.invocation.func(
ctx,
*value,
arg.a.invocation.argc,
arg.a.invocation.argv
), 0, NULL);
value = NULL;
}
if (!res.val.bool_)
break;
}
}
return res;
}
static struct buffer query_parse_table(
char *s, size_t len,
struct buffer buff,
struct invocation *invocation, const char **err)
{
unsigned i;
size_t _len;
unsigned argi = 0;
struct argument args[QUERY_MAX_ARGS];
const char *ident_name = NULL;
buff = query_chomp(buff);
buff = query_expect_char(s, len, buff, '{', err);
if (*err)
return buff;
buff = query_chomp(buff);
while (!query_peek(buff, "}", STRLEN_CONST("}")))
{
if (argi >= QUERY_MAX_ARGS)
{
strlcpy_lit(s, "Too many arguments in function call.", len);
*err = s;
goto clean;
}
if (ISALPHA((int)buff.data[buff.offset]))
{
buff = query_get_ident(s, len,
buff, &ident_name, &_len, err);
if (!*err)
{
args[argi].a.value.type = RDT_STRING;
args[argi].a.value.val.string.len = (uint32_t)_len;
args[argi].a.value.val.string.buff = (char*)calloc(
_len + 1, sizeof(char));
if (!args[argi].a.value.val.string.buff)
{
strlcpy_lit(s, "OOM", len);
*err = s;
goto clean;
}
/* strlcpy() would strlen() the source to compute its
* return value, and ident_name points into the query
* buffer, which is a (pointer, length) slice with no
* terminator of its own. Copy exactly the identifier. */
memcpy(args[argi].a.value.val.string.buff,
ident_name, _len);
args[argi].a.value.val.string.buff[_len] = '\0';
}
}
else
buff = query_parse_string(s, len,
buff, &args[argi].a.value, err);
if (*err)
goto clean;
args[argi].type = AT_VALUE;
buff = query_chomp(buff);
argi++;
buff = query_expect_char(s, len, buff, ':', err);
if (*err)
goto clean;
buff = query_chomp(buff);
if (argi >= QUERY_MAX_ARGS)
{
strlcpy_lit(s, "Too many arguments in function call.", len);
*err = s;
goto clean;
}
buff = query_parse_argument(s, len, buff, &args[argi], err);
if (*err)
goto clean;
argi++;
buff = query_chomp(buff);
buff = query_expect_char(s, len, buff, ',', err);
if (*err)
{
*err = NULL;
break;
}
buff = query_chomp(buff);
}
buff = query_expect_char(s, len, buff, '}', err);
if (*err)
goto clean;
invocation->func = query_func_all_map;
invocation->argc = argi;
invocation->argv = (struct argument*)
malloc(sizeof(struct argument) * argi);
if (!invocation->argv)
{
strlcpy_lit(s, "Out of memory.", len);
*err = s;
goto clean;
}
memcpy(invocation->argv, args,
sizeof(struct argument) * argi);
return buff;
clean:
for (i = 0; i < argi; i++)
query_argument_free(&args[i]);
return buff;
}
static struct buffer query_parse_method_call(
char *s, size_t len, struct buffer buff,
struct invocation *invocation, const char **err)
{
unsigned i;
size_t _len;
struct argument args[QUERY_MAX_ARGS];
unsigned argi = 0;
const char *func_name = NULL;
struct registered_func *rf = registered_functions;
invocation->func = NULL;
buff = query_get_ident(s, len, buff, &func_name, &_len, err);
if (*err)
return buff;
buff = query_chomp(buff);
buff = query_expect_char(s, len, buff, '(', err);
if (*err)
goto clean;
while (rf->name)
{
if (strncmp(rf->name, func_name, _len) == 0)
{
invocation->func = rf->func;
break;
}
rf++;
}
if (!invocation->func)
{
query_raise_unknown_function(s, len,
buff.offset, func_name, _len, err);
goto clean;
}
buff = query_chomp(buff);
while (!query_peek(buff, ")", STRLEN_CONST(")")))
{
if (argi >= QUERY_MAX_ARGS)
{
strlcpy_lit(s, "Too many arguments in function call.", len);
*err = s;
goto clean;
}
buff = query_parse_argument(s, len, buff, &args[argi], err);
if (*err)
goto clean;
argi++;
buff = query_chomp(buff);
buff = query_expect_char(s, len, buff, ',', err);
if (*err)
{
*err = NULL;
break;
}
buff = query_chomp(buff);
}
buff = query_expect_char(s, len, buff, ')', err);
if (*err)
goto clean;
invocation->argc = argi;
invocation->argv = (argi > 0) ? (struct argument*)
malloc(sizeof(struct argument) * argi) : NULL;
/* Gate the OOM branch on 'argi > 0 && !argv' - before this
* change a valid zero-arg function call ('foo()') was being
* erroneously treated as OOM because argv is legitimately
* NULL when argi==0. */
if (argi > 0 && !invocation->argv)
{
s[0] = 'O';
s[1] = 'O';
s[2] = 'M';
s[3] = '\0';
*err = s;
goto clean;
}
if (invocation->argv)
memcpy(invocation->argv, args,
sizeof(struct argument) * argi);
return buff;
clean:
for (i = 0; i < argi; i++)
query_argument_free(&args[i]);
return buff;
}
void libretrodb_query_free(void *q)
{
unsigned i;
struct query *real_q = (struct query*)q;
real_q->ref_count--;
if (real_q->ref_count > 0)
return;
for (i = 0; i < real_q->root.argc; i++)
query_argument_free(&real_q->root.argv[i]);
free(real_q->root.argv);
real_q->root.argv = NULL;
real_q->root.argc = 0;
free(real_q);
}
void *libretrodb_query_compile(libretrodb_t *db,
const char *query, size_t len, const char **err_string)
{
struct buffer buff;
/* Error text is formatted into storage owned by the db handle.
* This used to be a function-scope static, so the pointer returned
* through err_string was shared process-wide and two concurrent
* compiles clobbered each other's message - the second racy global
* in this file:
*
* WARNING: ThreadSanitizer: data race
* Location is global 'tmp_err_buff.0' of size 256
*
* When there is no db handle to borrow from, report a fixed
* message rather than writing anywhere. */
size_t err_buff_len = 0;
char *tmp_err_buff = libretrodb_query_err_buf(db, &err_buff_len);
struct query *q = (struct query*)malloc(sizeof(*q));
if (!q)
return NULL;
q->ref_count = 1;
q->ctx.intermediate_res.val.int_ = 0;
q->ctx.intermediate_res.val.uint_ = 0;
q->root.argc = 0;
q->root.func = NULL;
q->root.argv = NULL;
buff.data = query;
buff.len = len;
buff.offset = 0;
*err_string = NULL;
/* Every in-tree caller compiles against an open handle, and the
* parse helpers below format diagnostics straight into this
* buffer. Fail cleanly rather than teaching ten snprintf() sites
* to tolerate a NULL destination. */
if (!tmp_err_buff)
{
*err_string = "No database handle";
goto error;
}
buff = query_chomp(buff);
if (query_peek(buff, "{", STRLEN_CONST("{")))
{
buff = query_parse_table(tmp_err_buff,
err_buff_len, buff, &q->root, err_string);
if (*err_string)
goto error;
}
else if ( (size_t)buff.offset < buff.len
&& ISALPHA((int)buff.data[buff.offset]))
buff = query_parse_method_call(tmp_err_buff,
err_buff_len,
buff, &q->root, err_string);
buff = query_expect_eof(tmp_err_buff,
err_buff_len,
buff, err_string);
if (*err_string)
goto error;
if (!q->root.func)
{
if (tmp_err_buff)
{
snprintf(tmp_err_buff, err_buff_len,
"%" PRIu64 "::Unexpected EOF",
(uint64_t)buff.offset
);
*err_string = tmp_err_buff;
}
else
*err_string = "Unexpected EOF";
goto error;
}
return q;
error:
if (q)
libretrodb_query_free(q);
return NULL;
}
void libretrodb_query_inc_ref(libretrodb_query_t *q)
{
struct query *rq = (struct query*)q;
if (rq)
rq->ref_count += 1;
}
int libretrodb_query_filter(libretrodb_query_t *q,
struct rmsgpack_dom_value *v)
{
struct query *rq = (struct query *)q;
struct invocation inv = rq->root;
struct rmsgpack_dom_value res = inv.func(&rq->ctx, *v, inv.argc, inv.argv);
return (res.type == RDT_BOOL && res.val.bool_);
}
/**
* libretrodb_query_get_filter_fields:
*
* Extract the field names that a compiled table query filters on.
* For a query like {crc:or(b"..."), releaseyear:1995}, this returns
* pointers to "crc" and "releaseyear".
*
* Only works for table queries (root.func == query_func_all_map)
* where argv[even] entries are AT_VALUE / RDT_STRING field names.
*
* @q : Compiled query handle.
* @field_names : Output array of string pointers (not copied — valid
* for the lifetime of the query).
* @field_lens : Output array of string lengths.
* @max_fields : Capacity of output arrays.
*
* Returns: number of fields extracted, or 0 if the query structure
* is not a table query or has no extractable field names.
*/
int libretrodb_query_get_filter_fields(libretrodb_query_t *q,
const char **field_names, uint32_t *field_lens,
unsigned max_fields)
{
unsigned i;
unsigned count = 0;
struct query *rq = (struct query *)q;
if (!rq || !rq->root.func || !rq->root.argv)
return 0;
/* Table queries use query_func_all_map and store field names
* at even argv indices: argv[0]="crc", argv[1]=matcher,
* argv[2]="year", argv[3]=matcher, etc. */
if (rq->root.func != query_func_all_map)
return 0;
for (i = 0; i < rq->root.argc; i += 2)
{
struct argument *arg = &rq->root.argv[i];
if ( arg->type == AT_VALUE
&& arg->a.value.type == RDT_STRING
&& arg->a.value.val.string.buff)
{
if (count < max_fields)
{
field_names[count] = arg->a.value.val.string.buff;
field_lens[count] = arg->a.value.val.string.len;
}
count++;
}
}
return count;
}
/**
* libretrodb_query_eval_field:
*
* Evaluate a single field's query condition inline. For a table query
* like {crc:or(b"..."), year:1995}, this finds the condition matching
* @field_name and evaluates its matcher against @value.
*
* @q : Compiled query handle.
* @field_name : The map key name to evaluate (e.g. "crc").
* @field_len : Length of field_name.
* @value : The parsed DOM value for this field, or NULL to
* just check if the field is in the query.
*
* Returns: 1 if the condition passes (or field exists when value is NULL),
* 0 if the condition fails (mismatch),
* -1 if this field is not in the query (irrelevant).
*/
int libretrodb_query_eval_field(libretrodb_query_t *q,
const char *field_name, uint32_t field_len,
struct rmsgpack_dom_value *value)
{
unsigned i;
struct query *rq = (struct query *)q;
if (!rq || !rq->root.func || !rq->root.argv)
return -1;
if (rq->root.func != query_func_all_map)
return -1;
/* Walk argv pairs: argv[i] = field name, argv[i+1] = matcher */
for (i = 0; i + 1 < rq->root.argc; i += 2)
{
struct argument *key_arg = &rq->root.argv[i];
struct argument *val_arg = &rq->root.argv[i + 1];
if ( key_arg->type != AT_VALUE
|| key_arg->a.value.type != RDT_STRING
|| key_arg->a.value.val.string.len != field_len)
continue;
if (memcmp(key_arg->a.value.val.string.buff, field_name, field_len) != 0)
continue;
/* Found the matching condition */
/* If value is NULL, caller just wants to know if this
* field is in the query (existence check) */
if (!value)
return 1;
/* Evaluate the matcher against the value */
if (val_arg->type == AT_VALUE)
{
struct rmsgpack_dom_value res = func_equals(&rq->ctx, *value, 1, val_arg);
return res.val.bool_ ? 1 : 0;
}
else
{
struct rmsgpack_dom_value res = query_func_is_true(&rq->ctx,
val_arg->a.invocation.func(
&rq->ctx,
*value,
val_arg->a.invocation.argc,
val_arg->a.invocation.argv),
0, NULL);
return res.val.bool_ ? 1 : 0;
}
}
/* Field not in query — irrelevant */
return -1;
}