rizin/binrz/rz-test/run.c
2026-01-11 16:10:55 +08:00

608 lines
17 KiB
C

// SPDX-FileCopyrightText: 2020 thestr4ng3r <info@florianmaerkl.de>
// SPDX-License-Identifier: LGPL-3.0-only
#include "rz_test.h"
#include <rz_util/rz_str.h>
#include <rz_util/rz_regex.h>
#include <rz_cons.h>
#if __WINDOWS__
static ut8 *crlf2lf(ut8 *str) {
char *src = (char *)str;
char *dest = src;
while (*src) {
*dest = *src;
if (src[0] == '\r' && src[1] == '\n') {
// dest does not move
} else {
dest++;
}
src++;
}
*dest = '\0';
return str;
}
#else
#define crlf2lf(x) (x)
#endif
static RzSubprocessOutput *subprocess_runner(const char *file, const char *args[], size_t args_size,
const char *envvars[], const char *envvals[], size_t env_size, ut64 timeout_ms, void *user) {
RzSubprocess *proc = rz_subprocess_start(file, args, args_size, envvars, envvals, env_size);
if (!proc) {
return NULL;
}
RzSubprocessWaitReason r = rz_subprocess_wait(proc, timeout_ms);
if (r == RZ_SUBPROCESS_TIMEDOUT) {
rz_subprocess_kill(proc);
}
RzSubprocessOutput *out = rz_subprocess_drain(proc);
if (out) {
out->timeout = r == RZ_SUBPROCESS_TIMEDOUT;
out->out = crlf2lf(out->out);
out->err = crlf2lf(out->err);
}
rz_subprocess_free(proc);
return out;
}
#if __WINDOWS__
static char *convert_win_cmds(const char *cmds) {
char *r = malloc(strlen(cmds) + 1);
if (!r) {
return NULL;
}
char *p = r;
while (*cmds) {
if (*cmds == '!' || (*cmds == '\"' && cmds[1] == '!')) {
// Adjust shell syntax for Windows,
// only for lines starting with ! or "!
char c;
for (; c = *cmds, c; cmds++) {
if (c == '\\') {
// replace \$ by $
c = *++cmds;
if (c == '$') {
*p++ = '$';
} else {
*p++ = '\\';
*p++ = c;
}
} else if (c == '$') {
// replace ${VARNAME} by %VARNAME%
c = *++cmds;
if (c == '{') {
*p++ = '%';
cmds++;
for (; c = *cmds, c && c != '}'; cmds++) {
*p++ = c;
}
if (c) { // must check c to prevent overflow
*p++ = '%';
}
} else {
*p++ = '$';
*p++ = c;
}
} else {
*p++ = c;
if (c == '\n') {
cmds++;
break;
}
}
}
continue;
}
// Nothing to do, just copy the line
char *lend = strchr(cmds, '\n');
size_t llen;
if (lend) {
llen = lend - cmds + 1;
} else {
llen = strlen(cmds);
}
memcpy(p, cmds, llen);
cmds += llen;
p += llen;
}
*p = '\0';
return rz_str_replace(r, "/dev/null", "nul", true);
}
#endif
static RzSubprocessOutput *run_rz_test(RzTestRunConfig *config, ut64 timeout_ms, const char *cmds, RzList /*<char *>*/ *files, RzList /*<char *>*/ *extra_args, bool load_plugins, RzTestCmdRunner runner, void *user) {
RzPVector args;
rz_pvector_init(&args, NULL);
rz_pvector_push(&args, "-escr.utf8=0");
rz_pvector_push(&args, "-escr.color=0");
rz_pvector_push(&args, "-escr.interactive=0");
rz_pvector_push(&args, "-eflirt.sigdb.load.system=false");
rz_pvector_push(&args, "-esearch.show_progress=false");
rz_pvector_push(&args, "-eflirt.sigdb.load.home=false");
rz_pvector_push(&args, "-N");
RzListIter *it;
void *extra_arg, *file_arg;
rz_list_foreach (extra_args, it, extra_arg) {
rz_pvector_push(&args, extra_arg);
}
rz_pvector_push(&args, "-qc");
#if __WINDOWS__
char *wcmds = convert_win_cmds(cmds);
rz_pvector_push(&args, wcmds);
#else
rz_pvector_push(&args, (void *)cmds);
#endif
rz_list_foreach (files, it, file_arg) {
rz_pvector_push(&args, file_arg);
}
const char *envvars[] = {
#if __WINDOWS__
"ANSICON",
#endif
"RZ_NOPLUGINS"
};
const char *envvals[] = {
#if __WINDOWS__
"1",
#endif
"1"
};
#if __WINDOWS__
size_t env_size = load_plugins ? 1 : 2;
#else
size_t env_size = load_plugins ? 0 : 1;
#endif
RzSubprocessOutput *out = runner(config->rz_cmd, args.v.a, rz_pvector_len(&args), envvars, envvals, env_size, timeout_ms, user);
rz_pvector_clear(&args);
#if __WINDOWS__
free(wcmds);
#endif
return out;
}
RZ_API RzSubprocessOutput *rz_test_run_cmd_test(RzTestRunConfig *config, RzCmdTest *test, RzTestCmdRunner runner, void *user) {
RzList *extra_args = test->args.value ? rz_str_split_duplist(test->args.value, " ", true) : NULL;
RzList *files = test->file.value ? rz_str_split_duplist(test->file.value, "\n", true) : NULL;
RzListIter *it;
RzListIter *tmpit;
char *token;
rz_list_foreach_safe (extra_args, it, tmpit, token) {
if (!*token) {
rz_list_delete(extra_args, it);
}
}
rz_list_foreach_safe (files, it, tmpit, token) {
if (!*token) {
rz_list_delete(files, it);
}
}
if (rz_list_empty(files)) {
if (!files) {
files = rz_list_new();
} else {
files->free = NULL;
}
rz_list_push(files, "=");
}
ut64 timeout_ms = test->timeout.set ? test->timeout.value * 1000 : config->timeout_ms;
RzSubprocessOutput *out = run_rz_test(config, timeout_ms, test->cmds.value, files, extra_args, test->load_plugins, runner, user);
rz_list_free(extra_args);
rz_list_free(files);
return out;
}
RZ_API RZ_OWN RzStrBuf *rz_test_regex_full_match_str(RZ_NONNULL const char *pattern, RZ_NONNULL const char *text) {
return rz_regex_full_match_str(pattern, text, RZ_REGEX_ZERO_TERMINATED, RZ_REGEX_EXTENDED, RZ_REGEX_DEFAULT,
"\n");
}
RZ_API bool rz_test_cmp_cmd_output(const char *output, const char *expect, const char *regexp) {
if (regexp) {
RzStrBuf *match_str = rz_test_regex_full_match_str(regexp, output);
bool equal = false;
size_t expect_len = strlen(expect);
if (expect_len && expect[expect_len - 1] == '\n') {
// Ignore newline in expect
expect_len--;
size_t match_str_len = rz_strbuf_length(match_str);
if (match_str_len && rz_strbuf_get(match_str)[match_str_len - 1] == '\n') {
// Ignore newline in match_str
match_str_len--;
}
if (expect_len != match_str_len) {
rz_strbuf_free(match_str);
return false;
}
equal = !rz_str_cmp(expect, rz_strbuf_get(match_str), expect_len);
} else {
equal = RZ_STR_EQ(expect, rz_strbuf_get(match_str));
}
rz_strbuf_free(match_str);
return equal;
}
return !strcmp(expect, output);
}
RZ_API bool rz_test_check_cmd_test(RzSubprocessOutput *out, RzCmdTest *test) {
if (!out || out->ret != 0 || !out->out || !out->err || out->timeout) {
return false;
}
const char *expect_out = test->expect.value;
const char *regexp_out = test->regexp_out.value;
if (expect_out && !rz_test_cmp_cmd_output((char *)out->out, expect_out, regexp_out)) {
return false;
}
const char *expect_err = test->expect_err.value;
const char *regexp_err = test->regexp_err.value;
if (expect_err && !rz_test_cmp_cmd_output((char *)out->err, expect_err, regexp_err)) {
return false;
}
return true;
}
#define JQ_CMD "jq"
RZ_API bool rz_test_check_jq_available(void) {
const char *args[] = { "." };
const char *invalid_json = "this is not json lol";
RzSubprocess *proc = rz_subprocess_start(JQ_CMD, args, 1, NULL, NULL, 0);
if (proc) {
rz_subprocess_stdin_write(proc, (const ut8 *)invalid_json, strlen(invalid_json));
rz_subprocess_wait(proc, UT64_MAX);
}
bool invalid_detected = proc && rz_subprocess_ret(proc) != 0;
rz_subprocess_free(proc);
const char *valid_json = "{\"this is\":\"valid json\",\"lol\":true}";
proc = rz_subprocess_start(JQ_CMD, args, 1, NULL, NULL, 0);
if (proc) {
rz_subprocess_stdin_write(proc, (const ut8 *)valid_json, strlen(valid_json));
rz_subprocess_wait(proc, UT64_MAX);
}
bool valid_detected = proc && rz_subprocess_ret(proc) == 0;
rz_subprocess_free(proc);
return invalid_detected && valid_detected;
}
RZ_API RzSubprocessOutput *rz_test_run_json_test(RzTestRunConfig *config, RzJsonTest *test, RzTestCmdRunner runner, void *user) {
RzList *files = rz_list_new();
rz_list_push(files, (void *)config->json_test_file);
RzSubprocessOutput *ret = run_rz_test(config, config->timeout_ms, test->cmd, files, NULL, test->load_plugins, runner, user);
rz_list_free(files);
return ret;
}
RZ_API bool rz_test_check_json_test(RzSubprocessOutput *out, RzJsonTest *test) {
if (!out || out->ret != 0 || !out->out || !out->err || out->timeout) {
return false;
}
const char *args[] = { "." };
RzSubprocess *proc = rz_subprocess_start(JQ_CMD, args, 1, NULL, NULL, 0);
rz_subprocess_stdin_write(proc, (const ut8 *)out->out, strlen((char *)out->out));
rz_subprocess_wait(proc, UT64_MAX);
bool ret = rz_subprocess_ret(proc) == 0;
rz_subprocess_free(proc);
return ret;
}
RZ_API RzAsmTestOutput *rz_test_run_asm_test(RzTestRunConfig *config, RzAsmTest *test) {
RzAsmTestOutput *out = RZ_NEW0(RzAsmTestOutput);
if (!out) {
return NULL;
}
out->as_ret = out->disas_ret = out->il_ret = INT_MAX;
RzPVector args;
rz_pvector_init(&args, NULL);
if (test->arch) {
rz_pvector_push(&args, "-a");
rz_pvector_push(&args, (void *)test->arch);
}
if (test->cpu) {
rz_pvector_push(&args, "-c");
rz_pvector_push(&args, (void *)test->cpu);
}
char bits[0x20];
if (test->bits) {
snprintf(bits, sizeof(bits), "%d", test->bits);
rz_pvector_push(&args, "-b");
rz_pvector_push(&args, bits);
}
if (test->mode & RZ_ASM_TEST_MODE_BIG_ENDIAN) {
rz_pvector_push(&args, "-e");
}
char offset[0x20];
if (test->offset) {
rz_snprintf(offset, sizeof(offset), "0x%" PFMT64x, test->offset);
rz_pvector_push(&args, "-o");
rz_pvector_push(&args, offset);
}
if (test->mode & RZ_ASM_TEST_MODE_ASSEMBLE) {
rz_pvector_push(&args, test->disasm);
RzSubprocess *proc = rz_subprocess_start(config->rz_asm_cmd, args.v.a, rz_pvector_len(&args), NULL, NULL, 0);
if (rz_subprocess_wait(proc, config->timeout_ms) == RZ_SUBPROCESS_TIMEDOUT) {
rz_subprocess_kill(proc);
out->as_timeout = true;
goto rip;
}
char *as_err = (char *)crlf2lf(rz_subprocess_err(proc, NULL));
rz_str_trim(as_err);
out->as_err = as_err;
out->as_ret = rz_subprocess_ret(proc);
if (out->as_ret != 0) {
goto rip;
}
char *hex = (char *)crlf2lf(rz_subprocess_out(proc, NULL));
size_t hexlen = strlen(hex);
if (!hexlen) {
goto rip;
}
ut8 *bytes = malloc(hexlen);
int byteslen = rz_hex_str2bin(hex, bytes);
free(hex);
if (byteslen <= 0) {
free(bytes);
goto rip;
}
out->bytes = bytes;
out->bytes_size = (size_t)byteslen;
rip:
rz_pvector_pop(&args);
rz_subprocess_free(proc);
}
if (test->mode & RZ_ASM_TEST_MODE_DISASSEMBLE) {
char *hex = rz_hex_bin2strdup(test->bytes, test->bytes_size);
if (!hex) {
goto beach;
}
rz_pvector_push(&args, "-d");
rz_pvector_push(&args, hex);
RzSubprocess *proc = rz_subprocess_start(config->rz_asm_cmd, args.v.a, rz_pvector_len(&args), NULL, NULL, 0);
if (rz_subprocess_wait(proc, config->timeout_ms) == RZ_SUBPROCESS_TIMEDOUT) {
rz_subprocess_kill(proc);
out->disas_timeout = true;
goto ship;
}
char *disas_err = (char *)crlf2lf(rz_subprocess_err(proc, NULL));
rz_str_trim(disas_err);
out->disas_err = disas_err;
out->disas_ret = rz_subprocess_ret(proc);
if (out->disas_ret != 0) {
goto ship;
}
char *disasm = (char *)crlf2lf(rz_subprocess_out(proc, NULL));
rz_str_trim(disasm);
rz_str_replace_char(disasm, '\n', ';');
out->disasm = disasm;
ship:
free(hex);
rz_pvector_pop(&args);
rz_pvector_pop(&args);
rz_subprocess_free(proc);
}
if (test->il) {
char *hex = rz_hex_bin2strdup(test->bytes, test->bytes_size);
if (!hex) {
goto beach;
}
rz_pvector_push(&args, "-I");
rz_pvector_push(&args, hex);
RzSubprocess *proc = rz_subprocess_start(config->rz_asm_cmd, args.v.a, rz_pvector_len(&args), NULL, NULL, 0);
if (rz_subprocess_wait(proc, config->timeout_ms) == RZ_SUBPROCESS_TIMEDOUT) {
rz_subprocess_kill(proc);
out->il_timeout = true;
} else {
char *il = (char *)crlf2lf(rz_subprocess_out(proc, NULL));
rz_str_trim(il);
rz_str_replace_char(il, '\n', ';');
char *il_err = (char *)crlf2lf(rz_subprocess_err(proc, NULL));
rz_str_trim(il_err);
out->il = il;
out->il_err = il_err;
out->il_ret = rz_subprocess_ret(proc);
out->il_failed = out->il_ret != 0;
}
free(hex);
rz_pvector_pop(&args);
rz_pvector_pop(&args);
rz_subprocess_free(proc);
}
beach:
rz_pvector_clear(&args);
return out;
}
RZ_API bool rz_test_check_asm_test(RzAsmTestOutput *out, RzAsmTest *test) {
if (!out) {
return false;
}
if (test->mode & RZ_ASM_TEST_MODE_ASSEMBLE) {
if (!out->bytes || !test->bytes || out->bytes_size != test->bytes_size || out->as_timeout) {
return false;
}
if (memcmp(out->bytes, test->bytes, test->bytes_size) != 0) {
return false;
}
}
if (test->mode & RZ_ASM_TEST_MODE_DISASSEMBLE) {
if (!out->disasm || !test->disasm || out->as_timeout) {
return false;
}
if (strcmp(out->disasm, test->disasm) != 0) {
return false;
}
}
if (test->il) {
// expect some IL, no failure, no report and no timeout
if (!out->il || out->il_failed || RZ_STR_ISNOTEMPTY(out->il_err) || out->il_timeout) {
return false;
}
// IL must also be correct
if (strcmp(out->il, test->il) != 0) {
return false;
}
}
return true;
}
RZ_API void rz_test_asm_test_output_free(RzAsmTestOutput *out) {
if (!out) {
return;
}
free(out->disasm);
free(out->bytes);
free(out->il);
free(out->as_err);
free(out->disas_err);
free(out->il_err);
free(out);
}
RZ_API RzSubprocessOutput *rz_test_run_fuzz_test(RzTestRunConfig *config, RzFuzzTest *test, RzTestCmdRunner runner, void *user) {
const char *cmd = "e analysis.nopskip=0; aaa";
RzList *files = rz_list_new();
rz_list_push(files, test->file);
#if ASAN
if (rz_str_endswith(test->file, "/swift_read")) {
cmd = "?F";
}
#endif
RzSubprocessOutput *ret = run_rz_test(config, config->timeout_ms, cmd, files, NULL, false, runner, user);
rz_list_free(files);
return ret;
}
RZ_API bool rz_test_check_fuzz_test(RzSubprocessOutput *out) {
return out && out->ret == 0 && out->out && out->err && !out->timeout;
}
RZ_API char *rz_test_test_name(RzTest *test) {
switch (test->type) {
case RZ_TEST_TYPE_CMD:
if (test->cmd_test->name.value) {
return strdup(test->cmd_test->name.value);
}
return strdup("<unnamed>");
case RZ_TEST_TYPE_ASM:
return rz_str_newf("<asm> %s", test->asm_test->disasm ? test->asm_test->disasm : "");
case RZ_TEST_TYPE_JSON:
return rz_str_newf("<json> %s", test->json_test->cmd ? test->json_test->cmd : "");
case RZ_TEST_TYPE_FUZZ:
return rz_str_newf("<fuzz> %s", test->fuzz_test->file);
}
return NULL;
}
RZ_API bool rz_test_broken(RzTest *test) {
switch (test->type) {
case RZ_TEST_TYPE_CMD:
return test->cmd_test->broken.value;
case RZ_TEST_TYPE_ASM:
return test->asm_test->mode & RZ_ASM_TEST_MODE_BROKEN ? true : false;
case RZ_TEST_TYPE_JSON:
return test->json_test->broken;
case RZ_TEST_TYPE_FUZZ:
return false;
}
return false;
}
RZ_API RzTestResultInfo *rz_test_run_test(RzTestRunConfig *config, RzTest *test) {
RzTestResultInfo *ret = RZ_NEW0(RzTestResultInfo);
if (!ret) {
return NULL;
}
ret->test = test;
bool success = false;
ut64 start_time = rz_time_now_mono();
switch (test->type) {
case RZ_TEST_TYPE_CMD: {
RzCmdTest *cmd_test = test->cmd_test;
RzSubprocessOutput *out = rz_test_run_cmd_test(config, cmd_test, subprocess_runner, NULL);
success = rz_test_check_cmd_test(out, cmd_test);
ret->proc_out = out;
ret->timeout = out && out->timeout;
ret->run_failed = !out;
break;
}
case RZ_TEST_TYPE_ASM: {
RzAsmTest *asm_test = test->asm_test;
RzAsmTestOutput *out = rz_test_run_asm_test(config, asm_test);
success = rz_test_check_asm_test(out, asm_test);
ret->asm_out = out;
if (out) {
ret->timeout = out->as_timeout || out->disas_timeout || out->il_timeout;
}
ret->run_failed = !out;
break;
}
case RZ_TEST_TYPE_JSON: {
RzJsonTest *json_test = test->json_test;
RzSubprocessOutput *out = rz_test_run_json_test(config, json_test, subprocess_runner, NULL);
success = rz_test_check_json_test(out, json_test);
ret->proc_out = out;
if (out) {
ret->timeout = out->timeout;
}
ret->run_failed = !out;
break;
}
case RZ_TEST_TYPE_FUZZ: {
RzFuzzTest *fuzz_test = test->fuzz_test;
RzSubprocessOutput *out = rz_test_run_fuzz_test(config, fuzz_test, subprocess_runner, NULL);
success = rz_test_check_fuzz_test(out);
ret->proc_out = out;
if (out) {
ret->timeout = out->timeout;
}
ret->run_failed = !out;
}
}
ret->time_elapsed = rz_time_now_mono() - start_time;
bool broken = rz_test_broken(test);
#if ASAN
#if !RZ_ASSERT_STDOUT
#error RZ_ASSERT_STDOUT undefined or 0
#endif
RzSubprocessOutput *out = ret->proc_out;
if (!success && test->type == RZ_TEST_TYPE_CMD && strstr(test->path, "/dbg") && (!out->out || (!strstr((char *)out->out, "WARNING:") && !strstr((char *)out->out, "ERROR:") && !strstr((char *)out->out, "FATAL:"))) && (!out->err || (!strstr((char *)out->err, "Sanitizer") && !strstr((char *)out->err, "runtime error:")))) {
broken = true;
}
#endif
if (!success) {
ret->result = broken ? RZ_TEST_RESULT_BROKEN : RZ_TEST_RESULT_FAILED;
} else {
ret->result = broken ? RZ_TEST_RESULT_FIXED : RZ_TEST_RESULT_OK;
}
return ret;
}
RZ_API void rz_test_test_result_info_free(RzTestResultInfo *result) {
if (!result) {
return;
}
if (result->test) {
switch (result->test->type) {
case RZ_TEST_TYPE_CMD:
case RZ_TEST_TYPE_JSON:
case RZ_TEST_TYPE_FUZZ:
rz_subprocess_output_free(result->proc_out);
break;
case RZ_TEST_TYPE_ASM:
rz_test_asm_test_output_free(result->asm_out);
break;
}
}
free(result);
}