mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
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).
1311 lines
35 KiB
C
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;
|
|
}
|