rizin/binrz/rz-test/load.c
2026-03-18 03:51:18 +08:00

862 lines
20 KiB
C

// SPDX-FileCopyrightText: 2020 thestr4ng3r <info@florianmaerkl.de>
// SPDX-License-Identifier: LGPL-3.0-only
#include "rz_test.h"
#include <assert.h>
#define LINEFMT "%s, line %" PFMT64u ": "
RZ_API RzCmdTest *rz_test_cmd_test_new(void) {
return RZ_NEW0(RzCmdTest);
}
RZ_API void rz_test_cmd_test_free(RzCmdTest *test) {
if (!test) {
return;
}
#define DO_KEY_STR(key, field) free(test->field.value);
RZ_CMD_TEST_FOREACH_RECORD(DO_KEY_STR, RZ_CMD_TEST_FOREACH_RECORD_NOP, RZ_CMD_TEST_FOREACH_RECORD_NOP)
#undef DO_KEY_STR
free(test);
}
static char *readline(char *buf, size_t *linesz) {
char *end = strchr(buf, '\n');
if (end) {
size_t len = end - buf;
*end = '\0';
if (len > 0 && buf[len - 1] == '\r') {
buf[len - 1] = '\0';
len--;
}
*linesz = len;
return end + 1;
} else {
*linesz = strlen(buf);
return NULL;
}
}
// read the (possibly multiline) string value of some key in the file
// e.g. for
//
// 0 CMDS=<<EOF
// 1 Hello
// 2 World
// 3 EOF
// 4 ...
//
// if nextline is at the beginning of line 1,
// read_string_val(&nextline, "<<EOF\0")
// will return "Hello\nWorld\n" with nextline being at the beginning of line 4 afterwards.
static char *read_string_val(char **nextline, const char *val, ut64 *linenum) {
if (val[0] == '\'') {
size_t len = strlen(val);
if (len > 1 && val[len - 1] == '\'') {
eprintf("Error: Invalid string syntax, use <<EOF instead of '...'\n");
return NULL;
}
}
if (val[0] == '<' && val[1] == '<') {
// <<EOF syntax
const char *endtoken = val + 2;
if (!*endtoken) {
eprintf("Error: Missing opening end token after <<\n");
return NULL;
}
if (strcmp(endtoken, "EOF") != 0) {
// In case there will be strings containing "EOF" inside of them, this requirement
// can be weakened to only apply for strings which do not contain "EOF".
eprintf("Error: End token must be \"EOF\", got \"%s\" instead.", endtoken);
return NULL;
}
RzStrBuf *buf = rz_strbuf_new("");
char *line = *nextline;
size_t linesz = 0;
do {
*nextline = readline(line, &linesz);
(*linenum)++;
char *end = strstr(line, endtoken);
if (end != line) {
// Require the EOF to be at the beginning of the line.
// This means makes it impossible to write multiline tests without a trailing newline.
// This requirement could be lifted later if necessary.
end = NULL;
}
if (end) {
*end = '\0';
}
rz_strbuf_append(buf, line);
if (end) {
return rz_strbuf_drain(buf);
} else {
rz_strbuf_append(buf, "\n");
}
} while ((line = *nextline));
eprintf("Error: Missing closing end token %s\n", endtoken);
rz_strbuf_free(buf);
return NULL;
}
return strdup(val);
}
static const char *rz_test_tools[] = {
"rizin",
"rz-ar",
"rz-asm",
"rz-ax",
"rz-bin",
"rz-diff",
"rz-find",
"rz-gg",
"rz-hash",
"rz-run",
"rz-sign",
"rz-test",
};
RZ_API void rz_test_load_valid_tools(RZ_OUT RZ_NONNULL const char ***tools_o, RZ_OUT RZ_NONNULL size_t *size_o) {
rz_return_if_fail(tools_o && size_o);
*tools_o = rz_test_tools;
*size_o = RZ_ARRAY_SIZE(rz_test_tools);
}
static bool is_valid_tool(const char *tool) {
// we always whitelist the tools to avoid malware execution.
if (RZ_STR_ISEMPTY(tool)) {
// rz-test will run rizin or rz-asm
return true;
}
for (size_t i = 0; i < RZ_ARRAY_SIZE(rz_test_tools); ++i) {
if (RZ_STR_EQ(tool, rz_test_tools[i])) {
return true;
}
}
return false;
}
RZ_API RzPVector /*<RzCmdTest *>*/ *rz_test_load_cmd_test_file(const char *file) {
char *contents = rz_file_slurp(file, NULL);
if (!contents) {
eprintf("Failed to open file \"%s\"\n", file);
return NULL;
}
RzPVector *ret = rz_pvector_new(NULL);
if (!ret) {
free(contents);
return NULL;
}
RzCmdTest *test = rz_test_cmd_test_new();
if (!test) {
free(contents);
rz_pvector_free(ret);
return NULL;
}
ut64 linenum = 0;
char *line = contents;
size_t linesz;
char *nextline;
do {
nextline = readline(line, &linesz);
linenum++;
if (!linesz) {
continue;
}
if (*line == '#') {
continue;
}
char *val = strchr(line, '=');
if (val) {
*val = '\0';
val++;
} else if (!val && !(strcmp(line, "RUN") == 0)) {
eprintf("Error: No value for key \"%s\".\n", line);
goto fail;
}
// RUN is the only cmd without value
if (strcmp(line, "RUN") == 0) {
test->run_line = linenum;
if (RZ_STR_ISEMPTY(test->tool.value) && !test->cmds.value) {
eprintf(LINEFMT "Error: Rizin test without CMDS key\n", file, linenum);
goto fail;
} else if (RZ_STR_ISNOTEMPTY(test->tool.value) && !test->args.value) {
eprintf(LINEFMT "Error: Custom test without ARGS key\n", file, linenum);
goto fail;
} else if (!is_valid_tool(test->tool.value)) {
eprintf(LINEFMT "Error: TOOL key is set to '%s': this is not a valid rizin tool\n", file, linenum, test->tool.value);
goto fail;
}
if (!(test->expect.value || test->expect_err.value)) {
eprintf(LINEFMT "Error: Test without EXPECT or EXPECT_ERR key"
" (did you forget an EOF?)\n",
file, linenum);
goto fail;
}
rz_pvector_push(ret, test);
test = rz_test_cmd_test_new();
if (!test) {
eprintf("Error: Failed to allocate RzCmdTest\n");
goto beach;
}
continue;
}
#define DO_KEY_STR(key, field) \
if (strcmp(line, key) == 0) { \
if (test->field.value) { \
free(test->field.value); \
eprintf(LINEFMT "Warning: Duplicate key \"%s\"\n", file, linenum, key); \
} \
test->field.line_begin = linenum; \
test->field.value = read_string_val(&nextline, val, &linenum); \
test->field.line_end = linenum + 1; \
if (!test->field.value) { \
eprintf(LINEFMT "Error: Failed to read value for key \"%s\"\n", file, linenum, key); \
goto fail; \
} \
continue; \
}
#define DO_KEY_BOOL(key, field) \
if (strcmp(line, key) == 0) { \
if (test->field.value) { \
eprintf(LINEFMT "Warning: Duplicate key \"%s\"\n", file, linenum, key); \
} \
test->field.set = true; \
/* Strip comment */ \
char *cmt = strchr(val, '#'); \
if (cmt) { \
*cmt = '\0'; \
cmt--; \
while (cmt > val && *cmt == ' ') { \
*cmt = '\0'; \
cmt--; \
} \
} \
if (!strcmp(val, "1")) { \
test->field.value = true; \
} else if (!strcmp(val, "0")) { \
test->field.value = false; \
} else { \
eprintf(LINEFMT "Error: Invalid value \"%s\" for boolean key \"%s\", only \"1\" or \"0\" allowed.\n", file, linenum, val, key); \
goto fail; \
} \
continue; \
}
#define DO_KEY_NUM(key, field) \
if (strcmp(line, key) == 0) { \
if (test->field.value) { \
eprintf(LINEFMT "Warning: Duplicate key \"%s\"\n", file, linenum, key); \
} \
test->field.set = true; \
/* Strip comment */ \
char *cmt = strchr(val, '#'); \
if (cmt) { \
*cmt = '\0'; \
cmt--; \
while (cmt > val && *cmt == ' ') { \
*cmt = '\0'; \
cmt--; \
} \
} \
char *endval; \
test->field.value = strtol(val, &endval, 0); \
if (!endval || *endval) { \
eprintf(LINEFMT "Error: Invalid value \"%s\" for numeric key \"%s\", only numbers allowed.\n", file, linenum, val, key); \
goto fail; \
} \
continue; \
}
RZ_CMD_TEST_FOREACH_RECORD(DO_KEY_STR, DO_KEY_BOOL, DO_KEY_NUM)
#undef DO_KEY_STR
#undef DO_KEY_BOOL
#undef DO_KEY_NUM
eprintf(LINEFMT "Unknown key \"%s\".\n", file, linenum, line);
} while ((line = nextline));
beach:
free(contents);
if (test && (test->name.value || test->cmds.value || test->expect.value)) {
eprintf("Warning: found test tokens at the end of \"%s\" without RUN (line: %" PFMT64u ").\n", file, linenum);
}
rz_test_cmd_test_free(test);
return ret;
fail:
rz_test_cmd_test_free(test);
test = NULL;
rz_pvector_free(ret);
ret = NULL;
goto beach;
}
RZ_API RzAsmTest *rz_test_asm_test_new(void) {
return RZ_NEW0(RzAsmTest);
}
RZ_API void rz_test_asm_test_free(RzAsmTest *test) {
if (!test) {
return;
}
free(test->disasm);
free(test->bytes);
free(test->il);
free(test);
}
static bool parse_asm_path(const char *path, RzStrConstPool *strpool, const char **arch_out, const char **cpuout, int *bitsout) {
RzList *file_tokens = rz_str_split_duplist(path, RZ_SYS_DIR, true);
if (!file_tokens || rz_list_empty(file_tokens)) {
rz_list_free(file_tokens);
return false;
}
// Possibilities:
// arm
// arm_32
// arm_cortex_32
char *arch = rz_list_last_val(file_tokens);
if (!*arch) {
rz_list_free(file_tokens);
return false;
}
char *second = strchr(arch, '_');
if (second) {
*second = '\0';
second++;
char *third = strchr(second, '_');
if (third) {
*third = '\0';
third++;
*cpuout = rz_str_constpool_get(strpool, second);
*bitsout = atoi(third);
} else {
*cpuout = NULL;
*bitsout = atoi(second);
}
} else {
*cpuout = NULL;
*bitsout = 0;
}
*arch_out = rz_str_constpool_get(strpool, arch);
rz_list_free(file_tokens);
return true;
}
RZ_API RzPVector /*<RzAsmTest *>*/ *rz_test_load_asm_test_file(RzStrConstPool *strpool, const char *file) {
const char *arch;
const char *cpu;
int bits;
if (!parse_asm_path(file, strpool, &arch, &cpu, &bits)) {
eprintf("Failed to parse arch/cpu/bits from path %s\n", file);
return NULL;
}
char *contents = rz_file_slurp(file, NULL);
if (!contents) {
eprintf("Failed to open file \"%s\"\n", file);
return NULL;
}
RzPVector *ret = rz_pvector_new(NULL);
if (!ret) {
return NULL;
}
ut64 linenum = 0;
char *line = contents;
size_t linesz;
char *nextline;
do {
nextline = readline(line, &linesz);
linenum++;
if (!linesz) {
continue;
}
if (*line == '#') {
continue;
}
// mode flags
int mode = 0;
while (*line && *line != ' ') {
switch (*line) {
case 'a':
mode |= RZ_ASM_TEST_MODE_ASSEMBLE;
break;
case 'd':
mode |= RZ_ASM_TEST_MODE_DISASSEMBLE;
break;
case 'E':
mode |= RZ_ASM_TEST_MODE_BIG_ENDIAN;
break;
case 'B':
mode |= RZ_ASM_TEST_MODE_BROKEN;
break;
default:
eprintf(LINEFMT "Warning: Invalid mode char '%c'\n", file, linenum, *line);
goto fail;
}
line++;
}
if (!(mode & RZ_ASM_TEST_MODE_ASSEMBLE) && !(mode & RZ_ASM_TEST_MODE_DISASSEMBLE)) {
eprintf(LINEFMT "Warning: Mode specifies neither assemble nor disassemble.\n", file, linenum);
continue;
}
// disasm
char *disasm = strchr(line, '"');
if (!disasm) {
eprintf(LINEFMT "Error: Expected \" to begin disassembly.\n", file, linenum);
goto fail;
}
disasm++;
char *hex = strchr(disasm, '"');
if (!hex) {
eprintf(LINEFMT "Error: Expected \" to end disassembly.\n", file, linenum);
goto fail;
}
*hex = '\0';
hex++;
rz_str_trim(disasm);
// hex
while (*hex && *hex == ' ') {
hex++;
}
// remove comment at the end of the line
char *latecmt = strchr(hex, '#');
if (latecmt) {
*latecmt = '\0';
}
// offset (optional)
char *offset = strchr(hex, ' ');
char *il = NULL;
if (offset) {
*offset = '\0';
offset++;
// IL (optional, the entire rest)
il = strchr(offset, ' ');
if (il) {
*il = '\0';
il++;
}
}
size_t hexlen = strlen(hex);
if (!hexlen) {
eprintf(LINEFMT "Error: Expected hex chars.\n", file, linenum);
goto fail;
}
ut8 *bytes = malloc(hexlen);
if (!bytes) {
break;
}
int bytesz = rz_hex_str2bin(hex, bytes);
if (bytesz == 0) {
eprintf(LINEFMT "Error: Expected hex chars.\n", file, linenum);
free(bytes);
goto fail;
}
if (bytesz < 0) {
eprintf(LINEFMT "Error: Odd number of hex chars: %s\n", file, linenum, hex);
free(bytes);
goto fail;
}
RzAsmTest *test = rz_test_asm_test_new();
if (!test) {
free(bytes);
goto fail;
}
test->line = linenum;
test->bits = bits;
test->arch = arch;
test->cpu = cpu;
test->mode = mode;
char *endptr = NULL;
test->offset = offset ? (ut64)strtoull(offset, &endptr, 0) : 0;
if (endptr && *endptr) {
eprintf(LINEFMT "Error: Invalid offset string: \"%s\"\n", file, linenum, offset);
free(bytes);
goto fail;
}
test->disasm = strdup(disasm);
test->bytes = bytes;
test->bytes_size = (size_t)bytesz;
test->il = il ? strdup(il) : NULL;
rz_pvector_push(ret, test);
} while ((line = nextline));
beach:
free(contents);
return ret;
fail:
rz_pvector_free(ret);
ret = NULL;
goto beach;
}
RZ_API RzJsonTest *rz_test_json_test_new(void) {
return RZ_NEW0(RzJsonTest);
}
RZ_API void rz_test_json_test_free(RzJsonTest *test) {
if (!test) {
return;
}
free(test->cmd);
free(test);
}
RZ_API RzPVector /*<RzJsonTest *>*/ *rz_test_load_json_test_file(const char *file) {
char *contents = rz_file_slurp(file, NULL);
if (!contents) {
eprintf("Failed to open file \"%s\"\n", file);
return NULL;
}
RzPVector *ret = rz_pvector_new(NULL);
if (!ret) {
free(contents);
return NULL;
}
ut64 linenum = 0;
char *line = contents;
size_t linesz;
char *nextline;
do {
nextline = readline(line, &linesz);
linenum++;
if (!linesz) {
continue;
}
if (*line == '#') {
continue;
}
char *broken_token = strstr(line, "BROKEN");
if (broken_token) {
*broken_token = '\0';
}
rz_str_trim(line);
if (!*line) {
// empty line
continue;
}
RzJsonTest *test = rz_test_json_test_new();
if (!test) {
break;
}
test->line = linenum;
test->cmd = strdup(line);
if (!test->cmd) {
rz_test_json_test_free(test);
break;
}
test->broken = broken_token ? true : false;
rz_pvector_push(ret, test);
} while ((line = nextline));
free(contents);
return ret;
}
RZ_API void rz_test_fuzz_test_free(RzFuzzTest *test) {
if (!test) {
return;
}
free(test->file);
free(test);
}
RZ_API void rz_test_test_free(RzTest *test) {
if (!test) {
return;
}
switch (test->type) {
case RZ_TEST_TYPE_CMD:
rz_test_cmd_test_free(test->cmd_test);
break;
case RZ_TEST_TYPE_ASM:
rz_test_asm_test_free(test->asm_test);
break;
case RZ_TEST_TYPE_JSON:
rz_test_json_test_free(test->json_test);
break;
case RZ_TEST_TYPE_FUZZ:
rz_test_fuzz_test_free(test->fuzz_test);
break;
}
free(test);
}
RZ_API RzTestDatabase *rz_test_test_database_new(void) {
RzTestDatabase *db = RZ_NEW(RzTestDatabase);
if (!db) {
return NULL;
}
rz_pvector_init(&db->tests, (RzPVectorFree)rz_test_test_free);
rz_str_constpool_init(&db->strpool);
return db;
}
RZ_API void rz_test_test_database_free(RzTestDatabase *db) {
if (!db) {
return;
}
rz_pvector_clear(&db->tests);
rz_str_constpool_fini(&db->strpool);
free(db);
}
static RzTestType test_type_for_path(const char *path, bool *load_plugins) {
RzTestType ret = RZ_TEST_TYPE_CMD;
char *pathdup = strdup(path);
RzList *tokens = rz_str_split_list(pathdup, RZ_SYS_DIR, 0);
if (!tokens) {
return ret;
}
if (!rz_list_empty(tokens)) {
rz_list_pop(tokens);
}
RzListIter *it;
char *token;
*load_plugins = false;
rz_list_foreach (tokens, it, token) {
if (!strcmp(token, "asm")) {
ret = RZ_TEST_TYPE_ASM;
continue;
}
if (!strcmp(token, "json")) {
ret = RZ_TEST_TYPE_JSON;
continue;
}
if (!strcmp(token, "extras")) {
*load_plugins = true;
}
}
rz_list_free(tokens);
free(pathdup);
return ret;
}
static inline bool skip_archos(const char *subname) {
rz_return_val_if_fail(subname, true);
if (!strcmp(subname, RZ_TEST_ARCH_OS)) {
return false;
}
if (rz_str_startswith(subname, "not-")) {
const char *neg_subname = subname + strlen("not-");
const char *second_dash = strchr(neg_subname, '-');
rz_return_val_if_fail(second_dash, true);
if (strncmp(neg_subname, RZ_TEST_ARCH_OS, second_dash - neg_subname) &&
(rz_str_endswith(RZ_TEST_ARCH_OS, strrchr(subname, '-')) || rz_str_endswith(subname, "-any"))) {
return false;
}
} else {
if (rz_str_endswith(subname, "-any")) {
if (!strncmp(subname, RZ_TEST_ARCH_OS, strlen(subname) - strlen("-any"))) {
return false;
}
}
}
return true;
}
static bool database_load(RzTestDatabase *db, const char *path, int depth) {
if (depth <= 0) {
eprintf("Directories for loading tests too deep: %s\n", path);
return false;
}
if (rz_file_is_directory(path)) {
RzList *dir = rz_sys_dir(path);
if (!dir) {
return false;
}
RzListIter *it;
const char *subname;
RzStrBuf subpath;
rz_strbuf_init(&subpath);
bool ret = true;
rz_list_foreach (dir, it, subname) {
if (*subname == '.') {
continue;
}
if (!strcmp(subname, "extras")) {
// Only load "extras" dirs if explicitly specified
eprintf("Skipping %s" RZ_SYS_DIR "%s because it requires additional dependencies.\n", path, subname);
continue;
}
if ((!strcmp(path, "archos") || rz_str_endswith(path, RZ_SYS_DIR "archos")) && skip_archos(subname)) {
eprintf("Skipping %s" RZ_SYS_DIR "%s because it does not match the current platform.\n", path, subname);
continue;
}
rz_strbuf_setf(&subpath, "%s%s%s", path, RZ_SYS_DIR, subname);
if (!database_load(db, rz_strbuf_get(&subpath), depth - 1)) {
ret = false;
break;
}
}
rz_strbuf_fini(&subpath);
rz_list_free(dir);
return ret;
}
if (!rz_file_exists(path)) {
eprintf("Path \"%s\" does not exist\n", path);
return false;
}
// Not a directory but exists, load a file
const char *pooled_path = rz_str_constpool_get(&db->strpool, path);
bool load_plugins = false;
RzTestType test_type = test_type_for_path(path, &load_plugins);
switch (test_type) {
case RZ_TEST_TYPE_CMD: {
RzPVector *cmd_tests = rz_test_load_cmd_test_file(path);
if (!cmd_tests) {
return false;
}
void **it;
rz_pvector_foreach (cmd_tests, it) {
RzTest *test = RZ_NEW(RzTest);
if (!test) {
continue;
}
test->type = RZ_TEST_TYPE_CMD;
test->path = pooled_path;
test->cmd_test = *it;
test->cmd_test->load_plugins = load_plugins;
rz_pvector_push(&db->tests, test);
}
rz_pvector_free(cmd_tests);
break;
}
case RZ_TEST_TYPE_ASM: {
RzPVector *asm_tests = rz_test_load_asm_test_file(&db->strpool, path);
if (!asm_tests) {
return false;
}
void **it;
rz_pvector_foreach (asm_tests, it) {
RzTest *test = RZ_NEW(RzTest);
if (!test) {
continue;
}
test->type = RZ_TEST_TYPE_ASM;
test->path = pooled_path;
test->asm_test = *it;
rz_pvector_push(&db->tests, test);
}
rz_pvector_free(asm_tests);
break;
}
case RZ_TEST_TYPE_JSON: {
RzPVector *json_tests = rz_test_load_json_test_file(path);
if (!json_tests) {
return false;
}
void **it;
rz_pvector_foreach (json_tests, it) {
RzTest *test = RZ_NEW(RzTest);
if (!test) {
continue;
}
test->type = RZ_TEST_TYPE_JSON;
test->path = pooled_path;
test->json_test = *it;
test->json_test->load_plugins = load_plugins;
rz_pvector_push(&db->tests, test);
}
rz_pvector_free(json_tests);
break;
}
case RZ_TEST_TYPE_FUZZ:
// shouldn't come here, fuzz tests are loaded differently
break;
}
return true;
}
RZ_API bool rz_test_test_database_load(RzTestDatabase *db, const char *path) {
return database_load(db, path, 4);
}
static void database_load_fuzz_file(RzTestDatabase *db, const char *path, const char *file) {
RzFuzzTest *fuzz_test = RZ_NEW(RzFuzzTest);
if (!fuzz_test) {
return;
}
fuzz_test->file = strdup(file);
if (!fuzz_test->file) {
free(fuzz_test);
return;
}
RzTest *test = RZ_NEW(RzTest);
if (!test) {
free(fuzz_test->file);
free(fuzz_test);
return;
}
test->type = RZ_TEST_TYPE_FUZZ;
test->fuzz_test = fuzz_test;
test->path = rz_str_constpool_get(&db->strpool, path);
rz_pvector_push(&db->tests, test);
}
RZ_API bool rz_test_test_database_load_fuzz(RzTestDatabase *db, const char *path) {
if (rz_file_is_directory(path)) {
RzList *dir = rz_sys_dir(path);
if (!dir) {
return false;
}
RzListIter *it;
const char *subname;
RzStrBuf subpath;
rz_strbuf_init(&subpath);
bool ret = true;
rz_list_foreach (dir, it, subname) {
if (*subname == '.') {
continue;
}
rz_strbuf_setf(&subpath, "%s%s%s", path, RZ_SYS_DIR, subname);
if (rz_file_is_directory(rz_strbuf_get(&subpath))) {
// only load 1 level deep
continue;
}
database_load_fuzz_file(db, path, rz_strbuf_get(&subpath));
}
rz_strbuf_fini(&subpath);
rz_list_free(dir);
return ret;
}
if (!rz_file_exists(path)) {
eprintf("Path \"%s\" does not exist\n", path);
return false;
}
// Just a single file
database_load_fuzz_file(db, path, path);
return true;
}