rizin/test/integration/test_autocmplt.c
NOT XVilka d7aa7a664c
Add pf commands autocomplete (#6445)
* librz/core,cmd: add pf-aware autocompletion arg types

The `pf` family of commands (`pf`, `pf-`, `pfa`, `pfc`, `pfd`, `pf.`,
`pfn`, `pfo`, `pfs`, `pfv`, `pfw`) takes either a registered named
format, a `<format>.<field>[<idx>]...` path, or a Format Definition
File. None of these were autocompletable: the arg type was always
`RZ_CMD_ARG_TYPE_STRING`, so tab on `pf. <TAB>` did nothing and the
user had to remember every format and field name by hand.

Add three new arg types and wire them to the cmd_descs:

  * `RZ_CMD_ARG_TYPE_PF_FORMAT_NAME` enumerates the named formats
    from `rz_type_db_format_all()` and filters by the partial prefix.
    Used by `pf-`, `pfa`, `pfc`, `pfd`, `pfn`, `pfs`, `pfv`.

  * `RZ_CMD_ARG_TYPE_PF_FORMAT_PATH` is the path-aware completer for
    `pf.` and `pfw`. Both accept arbitrarily deep paths of the form
    `name[.field[<idx>]?]*`, walking through nested struct fields
    (e.g. `pf. troll.str[1].two` follows the same path syntax that
    `pf_path_navigate` accepts at runtime). The completer:
      - Finds the last `.` in the partial input; everything before
        it (inclusive) is the committed path, what follows is the
        segment being completed.
      - Walks each committed `name[N]?` segment in turn, looking up
        STRUCT fields' `type_name` in the typedb and re-parsing the
        referenced format. Descent past a scalar or an inline struct
        (no `type_name`) returns no options.
      - Offers the resolved format's field names for the tail.
      - Returns nothing when the tail contains `[` or `]` -- the
        user is mid-index or mid-descent, where identifier
        completion would produce a syntax error if accepted.
      - Suppresses the trailing space after each successful
        completion so `.` can be typed next without an inserted
        space getting in the way of descent.
      - Rewrites `res->start` past the last `.` so completion only
        replaces the tail; the committed path stays put.

  * `RZ_CMD_ARG_TYPE_PF_FDF_FILE` lists the basenames of FDFs found
    in the user's home formats dir and the system formats dir,
    mirroring the search order used by `pfo` itself; files present
    in both locations are reported once via the same `HtSU` de-dup
    used in `cmd_print_format_file_handler`. Used by `pfo`.

All three new types are added to `CD_ARG_LAST_TYPES` in
cmd_descs_util.py. The generator sets `RZ_CMD_ARG_FLAG_LAST` on the
final arg of a command whenever that arg's type is in this set;
that flag tells the runtime arg-preprocessor to merge any trailing
whitespace-separated tokens into a single argv slot. This is the
same implicit-FLAG_LAST treatment `RZ_CMD_ARG_TYPE_STRING` already
gets, and it is what keeps invocations like `pfc zd4x8 foo bar cow`
(five tokens, one logical format-with-names argument) working --
without it the cmd parser would reject the extra tokens with "Wrong
number of arguments". The implicit merge does not interfere with
autocompletion: the completer still receives the partial input up
to the cursor and prefix-matches against it, and the format-path
completer's dot/bracket scan is unaffected by whitespace.

The path completer uses a small helper, `pf_path_seg_consume`, that
parses one `name[N]?` segment with safe handling of unterminated `[`,
empty `[]`, non-numeric indices, and end-of-input. `pf_resolve_path_format`
walks all committed segments and returns the RzPfFormat the caller
should complete against; the resolver is the same shape as the
`pf_path_navigate` walker in librz/type/pf/pf_parser.c, except it
operates on RzPfFormat trees (typedb names) rather than RzPfValue
trees (decoded data), so it can run before any read has happened.

The completers live in `cautocmpl.c` next to the existing type-name
completers (`autocmplt_cmd_arg_struct_type` and friends) and follow
the same loop+strncmp pattern. The dispatcher entry for
`RZ_CMD_ARG_TYPE_FOLDER` was missing an explicit `break;` and would
fall through to `default`; harmless, but fixed in passing so the
three new cases sit cleanly above `default:`.

Note on dot-search direction: `rz_sub_str_rchr` is `start..end`
range search returning the FIRST hit, not a right-to-left "find
last" -- the `r` is for "range", not "right". The path completer
needs the last dot, so it walks the buffer backwards itself.

* test/integration: cover pf autocompletion

Thirteen new tests in test_autocmplt.c exercise the three new pf arg
types, including the multi-segment path resolver:

Format name completion:
  * `pf_format_name` -- `pfn ut_<TAB>` after registering two formats
    confirms both are offered.

Single-segment path completion:
  * `pf_format_path` -- three-phase walk through `pf. ut_path<TAB>`,
    `pf. ut_path.<TAB>`, `pf. ut_path.cou<TAB>`, covering name-only,
    dot-only, and dot-with-prefix. Verifies that `res->end_string`
    is empty in the name phase (so `.` can be typed next without an
    inserted space) and that `res->start` advances past the dot in
    the field phase (so the completion only replaces the field
    portion).
  * `pfw_format_path` -- the same `<format>.<field>` syntax must
    work on the write side too; confirms the PATH completer fires
    for `pfw` and is not pf.-specific.
  * `pf_format_path_empty` -- bare `pf. <TAB>` lists every
    registered format. Snapshots the baseline count first so the
    assertion stays robust against any default formats the type DB
    might seed.
  * `pf_format_path_unknown_name` -- `pf. nonexistent.<TAB>`
    returns an empty option list rather than crashing or leaking
    diagnostics.
  * `pf_format_path_anon_field` -- formats whose fields don't all
    have names (e.g. a `.` skip slot) must be iterated safely; the
    named fields are offered and the anonymous slot is silently
    dropped.

Multi-segment / nested-struct path completion:
  * `pf_format_path_nested` -- two-level descent through a STRUCT
    field whose `type_name` references another registered format,
    parsing the child format and offering its fields.
  * `pf_format_path_three_levels` -- three-level descent narrows
    correctly: A -> B -> C, then filter C's fields by a prefix.
  * `pf_format_path_array_index` -- `pf. troll.str[1].<TAB>` mirrors
    the existing cmd_pf2 runtime test; the array index in the
    middle segment is parsed and skipped (it doesn't change the
    target type).
  * `pf_format_path_inside_brackets` -- cursor inside an
    unclosed `[` returns no options (mid-index).
  * `pf_format_path_after_close_bracket` -- cursor right after `]`
    without a trailing `.` also returns no options (mid-descent).
  * `pf_format_path_through_scalar` -- descent past a scalar field
    is meaningless and returns no options.

The tests use plain `rz_core_new()` (the real cmd_descs already
registers all `pf*` commands), matching the pattern used by
`test_autocmplt_eco_themes`. Format strings use the parser's
"specifier-then-names" form (no internal whitespace in the spec
region) so `rz_pf_parse` produces the expected field count.

* doc,librz/core: align pf docs and `pf?` help with the parser

The standalone reference doc/pf.md and the in-tree `pf?` help (driven
by the details: block in librz/core/cmd_descs/cmd_print.yaml) had
drifted from each other and from what the parser actually accepts.
Both are now consistent with librz/type/pf/pf_parser.c.

Specific corrections:

  * `n` family. doc/pf.md claimed `N1`/`N2`/`N4`/`N8` existed as BE
    counterparts to `n1`-`n8`, and that bare `n`/`N` defaulted to
    `ctx.bits/8`. The parser handles only `n{1,2,4,8}`; all four
    forms are context-endian (follow `ctx->big_endian`), and bare
    `n` produces "unknown specifier". Rewrite the section to match,
    and explain why context-endian is the right choice for header
    readers like ELF.

  * Deprecation list. The `pf?` "deprecation" note listed `c, s, z`
    among the deprecated bare-letter codes -- they are not deprecated
    (`c` is the current 1-byte-as-char specifier, `s` is the current
    pointer-to-zstring, `z` is the current inline zstring). It was
    missing `C, i, Z, X, F, T` which the parser does warn on.
    doc/pf.md had `c` in its table marked "unchanged" (so it was
    visibly inconsistent with itself) and was missing the `x` row.
    Both lists now mirror the parser's PF_DIAG(DEPRECATED) call
    sites: b, C, d, f, F, i, o, q, t, T, w, x, X, Z.

  * TLV `h=`. `pf?` said "h=v/a (header inclusion)" (two options);
    the parser accepts `v` (value only, default), `l` (length covers
    len+value), and `a` (length covers tag+len+value). doc/pf.md
    already listed all three; help now matches.

  * `v(N)` bitvector. doc/pf.md documented this in detail (the
    1..4096-bit-wide field type used for things like ELF
    `DT_FLAGS_1`, PE characteristics, page-allocation maps), but the
    `pf?` help didn't mention it at all. Added to the DSL extensions
    section.

  * Pointer widths. doc/pf.md documented `p2`/`p4`/`p8` explicitly;
    `pf?` only mentioned bare `p` with a "size from ctx.bits"
    parenthetical. Help now lists the four forms in one entry.

  * GUID layouts. Both docs claimed `G(le)` was "all little-endian",
    but the renderer treats `G(le)` and `G(ms)` identically -- D4
    (the trailing 8 bytes) is always in buffer order regardless of
    layout. Document the actual behaviour rather than the implied
    one; this is a description fix, not a code change. Anyone who
    wants the byte-reversed-D4 reading can still file it as a
    follow-up bug against the renderer in pf_render.c.

cmd_descs.[ch] is regenerated automatically by the custom_target rule
when cmd_print.yaml changes; the .c diff in this commit is the result
of that regeneration (5 lines of comment text inside the existing
detail entries).
2026-06-02 13:44:07 +08:00

1178 lines
42 KiB
C

// SPDX-FileCopyrightText: 2021 ret2libc <sirmy15@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_core.h>
#include <rz_util/rz_str.h>
#include "../unit/minunit.h"
static RzCmdDescArg xd_args[] = {
{ .name = "f1", .type = RZ_CMD_ARG_TYPE_FILE },
{ .name = "F2", .type = RZ_CMD_ARG_TYPE_FCN },
{ .name = "e3", .type = RZ_CMD_ARG_TYPE_ENV },
{ .name = "Z4", .type = RZ_CMD_ARG_TYPE_REG_TYPE },
{ .name = "E5", .type = RZ_CMD_ARG_TYPE_EVAL_FULL },
{ 0 },
};
static RzCmdDescHelp xd_help = {
.summary = "xd summary",
.args = xd_args,
};
static RzCmdDescArg xr_args[] = {
{ .name = "D1", .type = RZ_CMD_ARG_TYPE_FOLDER },
{ 0 },
};
static RzCmdDescHelp xr_help = {
.summary = "xr summary",
.args = xr_args,
};
static RzCmdDescArg xe_args[] = {
{ .name = "f1", .type = RZ_CMD_ARG_TYPE_STRING },
{ 0 },
};
static RzCmdDescHelp xe_help = {
.summary = "xe summary",
.args = xe_args,
};
static RzCmdDescHelp x_group_help = {
.summary = "x group summary",
};
static RzCmdDescHelp p_help = {
.summary = "p summary",
.args = xe_args,
};
static RzCmdDescArg s_args[] = {
{ .name = "v1", .type = RZ_CMD_ARG_TYPE_RZNUM, .flags = RZ_CMD_ARG_FLAG_LAST },
{ 0 },
};
static RzCmdDescHelp s_help = {
.summary = "s summary",
.args = s_args,
};
static char **z_args_choices_cb(RzCore *core) {
char **res = RZ_NEWS0(char *, 3);
res[0] = strdup("Hello");
res[1] = strdup("World");
return res;
}
static RzCmdDescArg z_args[] = {
{ .name = "v1", .type = RZ_CMD_ARG_TYPE_CHOICES, .choices.choices_cb = z_args_choices_cb },
{ 0 },
};
static RzCmdDescHelp z_help = {
.summary = "z summary",
.args = z_args,
};
static RzCmdStatus x_handler(RzCore *core, int argc, const char **argv) {
return RZ_CMD_STATUS_OK;
}
static RzCmd *old_rcmd = NULL;
static RzCore *fake_core_new(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzCoreFile *cf = rz_core_file_open(core, "bins/elf/hello_world", RZ_PERM_R, 0);
mu_assert_notnull(cf, "file should be opened");
rz_core_bin_load(core, "bins/elf/hello_world", 0);
old_rcmd = core->rcmd;
RzCmd *cmd = rz_core_cmd_new(core, true);
mu_assert_notnull(cmd, "cmd should be created");
RzCmdDesc *root = rz_cmd_get_root(cmd);
mu_assert_notnull(root, "root should be present");
RzCmdDesc *x = rz_cmd_desc_group_new(cmd, root, "x", NULL, NULL, &x_group_help);
mu_assert_notnull(x, "x");
RzCmdDesc *xd = rz_cmd_desc_argv_new(cmd, x, "xd", x_handler, &xd_help);
mu_assert_notnull(xd, "xd");
RzCmdDesc *xr = rz_cmd_desc_argv_new(cmd, x, "xr", x_handler, &xr_help);
mu_assert_notnull(xr, "xr");
RzCmdDesc *xe = rz_cmd_desc_argv_new(cmd, x, "xe", x_handler, &xe_help);
mu_assert_notnull(xe, "xe");
RzCmdDesc *p = rz_cmd_desc_argv_new(cmd, root, "p", x_handler, &p_help);
mu_assert_notnull(p, "p");
RzCmdDesc *s = rz_cmd_desc_argv_new(cmd, root, "s", x_handler, &s_help);
mu_assert_notnull(s, "s");
RzCmdDesc *z = rz_cmd_desc_argv_new(cmd, root, "z", x_handler, &z_help);
mu_assert_notnull(z, "z");
core->rcmd = cmd;
rz_core_cmd(core, "", 0);
return core;
}
static void fake_core_free(RzCore *core) {
rz_cmd_free(core->rcmd);
core->rcmd = old_rcmd;
rz_core_free(core);
}
static RzCore *fake_core_new2(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzCoreFile *cf = rz_core_file_open(core, "bins/elf/hello_world", RZ_PERM_R, 0);
mu_assert_notnull(cf, "file should be opened");
rz_core_bin_load(core, "bins/elf/hello_world", 0);
RzCmdDesc *root = rz_cmd_get_root(core->rcmd);
mu_assert_notnull(root, "root should be present");
RzCmdDesc *unittest_cd = rz_cmd_desc_argv_new(core->rcmd, root, "unittest", x_handler, &xd_help);
mu_assert_notnull(unittest_cd, "unittest_cd");
rz_core_cmd(core, "", 0);
return core;
}
static bool test_autocmplt_cmdid(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
strcpy(buf->data, "x");
buf->length = strlen("x");
buf->index = 1;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_eq(r->start, 0, "should autocomplete starting from 0");
mu_assert_eq(r->end, 1, "should autocomplete ending at 1");
mu_assert_eq(rz_pvector_len(&r->options), 3, "there are 3 commands starting with `x`");
mu_assert_streq(rz_pvector_at(&r->options, 0), "xd", "one is xd");
mu_assert_streq(rz_pvector_at(&r->options, 1), "xr", "one is xr");
mu_assert_streq(rz_pvector_at(&r->options, 2), "xe", "one is xe");
rz_line_ns_completion_result_free(r);
strcpy(buf->data, "p @@c:x");
buf->length = strlen("p @@c:x");
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should be returned");
mu_assert_eq(r->start, buf->length - 1, "start is ok");
mu_assert_eq(r->end, buf->length, "end is ok");
mu_assert_eq(rz_pvector_len(&r->options), 3, "there are 3 commands starting with `x`");
mu_assert_streq(rz_pvector_at(&r->options, 0), "xd", "one is xd");
mu_assert_streq(rz_pvector_at(&r->options, 1), "xr", "one is xr");
mu_assert_streq(rz_pvector_at(&r->options, 2), "xe", "one is xe");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_newcommand(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
strcpy(buf->data, "p @@c:");
buf->length = strlen("p @@c:");
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result should be there");
mu_assert_eq(r->start, buf->length, "start should be ok");
mu_assert_eq(r->end, buf->length, "end should be ok");
mu_assert_eq(rz_pvector_len(&r->options), 6, "there are 4 commands available");
mu_assert_streq(rz_pvector_at(&r->options, 0), "p", "one is p");
mu_assert_streq(rz_pvector_at(&r->options, 1), "s", "one is s");
mu_assert_streq(rz_pvector_at(&r->options, 2), "xd", "one is xd");
mu_assert_streq(rz_pvector_at(&r->options, 3), "xr", "one is xr");
mu_assert_streq(rz_pvector_at(&r->options, 4), "xe", "one is xe");
mu_assert_streq(rz_pvector_at(&r->options, 5), "z", "one is z");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_argid(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "xd ./unit/test_interv";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, 3, "should autocomplete starting from ./...");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 1, "there is just one file with test_interv");
mu_assert_streq(rz_pvector_at(&r->options, 0), "." RZ_SYS_DIR "unit" RZ_SYS_DIR "test_intervaltree.c", "test_intervaltree.c");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_quotedarg(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "xd \"./unit/test_interv";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, 4, "should autocomplete starting from ./...");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 1, "there is just one file with test_interv");
mu_assert_streq(rz_pvector_at(&r->options, 0), "." RZ_SYS_DIR "unit" RZ_SYS_DIR "test_intervaltree.c", "test_intervaltree.c");
mu_assert_streq(r->end_string, "\" ", "double quotes should be put at the end of the string");
rz_line_ns_completion_result_free(r);
s = "xd './unit/test_interv";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, 4, "should autocomplete starting from ./...");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 1, "there is just one file with test_interv");
mu_assert_streq(rz_pvector_at(&r->options, 0), "." RZ_SYS_DIR "unit" RZ_SYS_DIR "test_intervaltree.c", "test_intervaltree.c");
mu_assert_streq(r->end_string, "' ", "double quotes should be put at the end of the string");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_newarg(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
char *cwd = rz_sys_getdir();
rz_sys_mkdir("newarg_test");
rz_sys_chdir("newarg_test");
mu_assert_true(rz_file_touch("file0"), "");
mu_assert_true(rz_file_touch("file1"), "");
mu_assert_true(rz_file_touch("file2"), "");
const char *s = "xd ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, buf->length, "should autocomplete starting after space");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 3, "there are 3 files in newarg_test dir");
bool found[3] = { false, false, false };
void **it;
rz_pvector_foreach (&r->options, it) {
char *f = *(char **)it;
mu_assert_true(rz_str_startswith(f, "." RZ_SYS_DIR "file"), "options start with ./file");
int v = atoi(f + strlen("./file"));
found[v] = true;
}
mu_assert_true(found[0], "file0 found");
mu_assert_true(found[1], "file1 found");
mu_assert_true(found[2], "file2 found");
rz_line_ns_completion_result_free(r);
rz_file_rm("file0");
rz_file_rm("file1");
rz_file_rm("file2");
rz_file_rm("newarg_test");
rz_sys_chdir(cwd);
free(cwd);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_arg_folder(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
char *cwd = rz_sys_getdir();
rz_sys_mkdir("New_test_folder");
rz_sys_chdir("New_test_folder");
rz_sys_mkdir("test_folder_1");
rz_sys_mkdir("test_folder_2");
mu_assert_true(rz_file_touch("test_file_1"), "create test_file_1");
mu_assert_true(rz_file_touch("test_file_2"), "create test_file_2");
const char *s = "xr ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "completion result should not be null for command");
mu_assert_eq(r->start, buf->length, "should autocomplete starting from position");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
size_t found_folder_1 = 0, found_folder_2 = 0, found_files = 0;
void **it;
rz_pvector_foreach (&r->options, it) {
const char *f = *(const char **)it;
if (rz_file_is_directory(f)) {
if (rz_str_strchr(f, "test_folder_1"))
found_folder_1 = 1;
if (rz_str_strchr(f, "test_folder_2"))
found_folder_2 = 1;
} else {
found_files++;
}
}
mu_assert_true(found_folder_1, "test_folder_1 should be in completions");
mu_assert_true(found_folder_2, "test_folder_2 should be in completions");
mu_assert_eq(found_files, 0, "NO files should be in folder type completions");
rz_line_ns_completion_result_free(r);
rz_file_rm("test_file_1");
rz_file_rm("test_file_2");
rz_file_rm("test_folder_1");
rz_file_rm("test_folder_2");
rz_file_rm("New_test_folder");
rz_sys_chdir(cwd);
free(cwd);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_fcn(void) {
RzCore *core = fake_core_new2();
mu_assert_notnull(core, "core not null");
RzLineBuffer *buf = &core->cons->line->buffer;
rz_core_analysis_all(core);
const char *s = "unittest ./file2 sym.imp.s";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("unittest ./file1 "), "should autocomplete starting after space");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 3, "there are 3 functions starting with sym.imp.s");
mu_assert_streq(rz_pvector_at(&r->options, 0), "sym.imp.strlen", "strlen");
mu_assert_streq(rz_pvector_at(&r->options, 1), "sym.imp.strcpy", "strcpy");
mu_assert_streq(rz_pvector_at(&r->options, 2), "sym.imp.strcat", "strcat");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
static bool test_autocmplt_eval(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "xd 1 2 3 4 asm.lines.w";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("xd 1 2 3 4 "), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 config evals starting with asm.lines.w");
mu_assert_streq(rz_pvector_at(&r->options, 0), "asm.lines.wide", "asm.lines.wide found");
mu_assert_streq(rz_pvector_at(&r->options, 1), "asm.lines.width", "asm.lines.width found");
rz_line_ns_completion_result_free(r);
s = "xd 1 2 3 4 search.in=io.maps.r";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("xd 1 2 3 4 search.in="), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 4, "there are 4 options values for config eval search.in");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_seek(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "s ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("s "), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 177, "there are 177 rznum vars on loading");
rz_line_ns_completion_result_free(r);
rz_flag_set(core->flags, "flag1", 0x1000, 1);
rz_flag_set(core->flags, "flag2", 0x2000, 1);
rz_flag_set(core->flags, "test3", 0x3000, 1);
rz_flag_set(core->flags, "test4", 0x4000, 1);
s = "s fl";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("s "), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 rznum vars starting with fl");
mu_assert_streq(rz_pvector_at(&r->options, 0), "flag1", "flag1 found");
mu_assert_streq(rz_pvector_at(&r->options, 1), "flag2", "flag2 found");
rz_line_ns_completion_result_free(r);
s = "s flag1 + tes";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("s flag1 + "), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 rznum vars starting with tes");
mu_assert_streq(rz_pvector_at(&r->options, 0), "test3", "test3 found");
mu_assert_streq(rz_pvector_at(&r->options, 1), "test4", "test4 found");
rz_line_ns_completion_result_free(r);
s = "s flag1+tes";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("s flag1+"), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 rznum vars starting with tes");
mu_assert_streq(rz_pvector_at(&r->options, 0), "test3", "test3 found");
mu_assert_streq(rz_pvector_at(&r->options, 1), "test4", "test4 found");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_global(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should not be null");
RzAnalysisVarGlobal *glob1 = rz_analysis_var_global_new("GINT", 0x1337); // untyped global
mu_assert_notnull(glob1, "glob1 null");
bool added = rz_analysis_var_global_add(core->analysis, glob1);
mu_assert_true(added, "unable to add glob1");
RzAnalysisVarGlobal *glob2 = rz_analysis_var_global_new("GCHR", 0xd3ad); // typed global
mu_assert_notnull(glob2, "glob2 null");
added = rz_analysis_var_global_add(core->analysis, glob2);
mu_assert_true(added, "unable to add glob2");
RzTypeParser *parser = rz_type_parser_new();
mu_assert_notnull(parser, "create type parser");
char *errmsg = NULL;
RzType *typ = rz_type_parse_string_single(parser, "int", &errmsg);
free(errmsg);
mu_assert_notnull(typ, "parsed type");
rz_analysis_var_global_set_type(glob2, typ);
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "avgl ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("avgl "), "should autocomplete the last arg");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 global vars");
mu_assert_streq(rz_pvector_at(&r->options, 0), "GINT", "GINT found");
mu_assert_streq(rz_pvector_at(&r->options, 1), "GCHR", "GCHR found");
rz_line_ns_completion_result_free(r);
rz_type_parser_free(parser);
rz_core_free(core);
mu_end;
}
static bool test_autocmplt_tmp_operators(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pd @";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd "), "should autocomplete the @ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
const char *tmp_ops[] = {
"@ ",
"@!",
"@(",
"@a:",
"@b:",
"@B:",
"@e:",
"@f:",
"@F:",
"@i:",
"@k:",
"@o:",
"@r:",
"@s:",
"@v:",
"@x:",
"@@.",
"@@=",
"@@@=",
"@@",
"@@c:",
"@@@c:",
"@@C",
"@@C:",
"@@dbt",
"@@dbtb",
"@@dbts",
"@@t",
"@@b",
"@@i",
"@@ii",
"@@iS",
"@@iSS",
"@@is",
"@@iz",
"@@f",
"@@f:",
"@@F",
"@@F:",
"@@om",
"@@dm",
"@@r",
"@@s:",
};
mu_assert_eq(rz_pvector_len(&r->options), RZ_ARRAY_SIZE(tmp_ops), "there are all @/@@/@@ operators (see @?, @@?)");
int i;
for (i = 0; i < RZ_ARRAY_SIZE(tmp_ops); i++) {
char msg[100];
rz_strf(msg, "%d-th should be %s", i, tmp_ops[i]);
mu_assert_streq(rz_pvector_at(&r->options, i), tmp_ops[i], msg);
}
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_iter_operators(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pd @@";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd "), "should autocomplete the @@ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
const char *iter_ops[] = {
"@@.",
"@@=",
"@@@=",
"@@",
"@@c:",
"@@@c:",
"@@C",
"@@C:",
"@@dbt",
"@@dbtb",
"@@dbts",
"@@t",
"@@b",
"@@i",
"@@ii",
"@@iS",
"@@iSS",
"@@is",
"@@iz",
"@@f",
"@@f:",
"@@F",
"@@F:",
"@@om",
"@@dm",
"@@r",
"@@s:",
};
mu_assert_eq(rz_pvector_len(&r->options), RZ_ARRAY_SIZE(iter_ops), "there are all @@/@@ operators (see @@?)");
int i;
for (i = 0; i < RZ_ARRAY_SIZE(iter_ops); i++) {
char msg[100];
rz_strf(msg, "%d-th should be %s", i, iter_ops[i]);
mu_assert_streq(rz_pvector_at(&r->options, i), iter_ops[i], msg);
}
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_tmp_seek(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pd @ ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd @ "), "should autocomplete the @ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_true(rz_pvector_len(&r->options) > 100, "there are a lot of possible values to seek to");
rz_line_ns_completion_result_free(r);
s = "pd @ st";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd @ "), "should autocomplete the @ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 15, "there are 15 possible values to seek to starting with st");
mu_assert_streq(rz_pvector_at(&r->options, 13), "str.Hello", "hello string is there");
mu_assert_streq(rz_pvector_at(&r->options, 14), "str.r2_folks", "r2_folks string is there");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_tmp_config(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pd @e:";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd @e:"), "should autocomplete the @ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_true(rz_pvector_len(&r->options) > 20, "there are many possible eval vars");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_tmp_arch(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pd @a:w";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, strlen("pd @a:"), "should autocomplete the @ operator");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 1, "there is just 1 arch starting with w: wasm");
mu_assert_streq(rz_pvector_at(&r->options, 0), "wasm", "hello string is there");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_choices_cb_arg(void) {
RzCore *core = fake_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "z ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "r should not be null");
mu_assert_eq(r->start, buf->length, "should autocomplete starting after space");
mu_assert_eq(r->end, buf->length, "should autocomplete ending at end of buffer");
mu_assert_eq(rz_pvector_len(&r->options), 2, "there are 2 choices from cb");
mu_assert_streq(rz_pvector_at(&r->options, 0), "Hello", "hello choice is there");
mu_assert_streq(rz_pvector_at(&r->options, 1), "World", "world choice is there");
rz_line_ns_completion_result_free(r);
fake_core_free(core);
mu_end;
}
static bool test_autocmplt_eco_themes(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "eco ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "Autocomplete result should not be NULL");
size_t count = rz_pvector_len(&r->options);
mu_assert_true(count > 0, "There should be at least one theme");
mu_assert_streq(rz_pvector_at(&r->options, 0), "ayu", "First theme should be ayu or similar");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Register two named `pf` formats and confirm `pfn <TAB>` lists them. */
static bool test_autocmplt_pf_format_name(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_alpha", "x4 magic");
rz_type_db_format_set(typedb, "ut_beta", "x4d4 magic count");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pfn ut_";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result should be returned");
mu_assert_eq(rz_pvector_len(&r->options), 2, "two formats start with 'ut_'");
/* Order is whatever `rz_type_db_format_all` returns; verify membership. */
bool saw_alpha = false, saw_beta = false;
for (size_t i = 0; i < rz_pvector_len(&r->options); i++) {
const char *opt = rz_pvector_at(&r->options, i);
if (!strcmp(opt, "ut_alpha")) {
saw_alpha = true;
}
if (!strcmp(opt, "ut_beta")) {
saw_beta = true;
}
}
mu_assert_true(saw_alpha, "ut_alpha should be offered");
mu_assert_true(saw_beta, "ut_beta should be offered");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* `pf.` is the named-display command and accepts `<name>.<field>` paths.
* With no dot in the partial, completion lists format names; once a dot
* is typed, completion descends into the named format's top-level field
* names and rewrites `start` so only the field portion is replaced. */
static bool test_autocmplt_pf_format_path(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_path", "x4d4z magic count name");
RzLineBuffer *buf = &core->cons->line->buffer;
/* Phase 1: no dot yet -- complete format name. */
const char *s1 = "pf. ut_pa";
strcpy(buf->data, s1);
buf->length = strlen(s1);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "phase1 result");
mu_assert_eq(rz_pvector_len(&r->options), 1, "one format matches 'ut_pa'");
mu_assert_streq(rz_pvector_at(&r->options, 0), "ut_path", "format name");
mu_assert_streq(r->end_string, "", "no trailing space so the user can type '.' next");
rz_line_ns_completion_result_free(r);
/* Phase 2: dot typed, prefix is empty -- list all top-level fields. */
const char *s2 = "pf. ut_path.";
strcpy(buf->data, s2);
buf->length = strlen(s2);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "phase2 result");
mu_assert_eq(rz_pvector_len(&r->options), 3, "three fields: magic, count, name");
/* start should point past the dot so only the field is replaced. */
mu_assert_eq(r->start, buf->length, "start advanced to post-dot");
bool saw_magic = false, saw_count = false, saw_name = false;
for (size_t i = 0; i < rz_pvector_len(&r->options); i++) {
const char *opt = rz_pvector_at(&r->options, i);
if (!strcmp(opt, "magic")) {
saw_magic = true;
}
if (!strcmp(opt, "count")) {
saw_count = true;
}
if (!strcmp(opt, "name")) {
saw_name = true;
}
}
mu_assert_true(saw_magic && saw_count && saw_name, "all three field names offered");
rz_line_ns_completion_result_free(r);
/* Phase 3: dot + field prefix -- narrow to matching field. */
const char *s3 = "pf. ut_path.cou";
strcpy(buf->data, s3);
buf->length = strlen(s3);
buf->index = buf->length;
r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "phase3 result");
mu_assert_eq(rz_pvector_len(&r->options), 1, "only 'count' starts with 'cou'");
mu_assert_streq(rz_pvector_at(&r->options, 0), "count", "narrowed to count");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* `pfw <format>` also takes a `name.field` path (the write side of the
* same syntax `pf.` uses). Make sure the same PATH completer fires here
* too -- not a separate code path. */
static bool test_autocmplt_pfw_format_path(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_w", "x4x4 a b");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pfw ut_w.";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 2, "two fields: a, b");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* `pf.` with no argument and no partial should list every registered
* format. This is the discovery use case ("what formats do I have?"). */
static bool test_autocmplt_pf_format_path_empty(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
/* Snapshot what's already registered (the default type DB seeds a
* handful of named formats) so the count assertion stays robust. */
RzList *baseline = rz_type_db_format_all(typedb);
size_t baseline_len = baseline ? rz_list_length(baseline) : 0;
rz_list_free(baseline);
rz_type_db_format_set(typedb, "ut_empty_test", "x4x magic v");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), baseline_len + 1,
"all existing formats plus the one we just added");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Unknown format name in front of the dot: no fields to descend into,
* so the completer returns an empty option list (instead of crashing or
* leaking the typedb error path). */
static bool test_autocmplt_pf_format_path_unknown_name(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_not_a_format.";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 0, "no options for unknown name");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Fields without an explicit name (anonymous, e.g. skip/align) must be
* skipped silently rather than crashing on a NULL `fields[i].name`. The
* format below mixes a named field with an unnamed skip slot. */
static bool test_autocmplt_pf_format_path_anon_field(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
/* `.` is the skip specifier and has no associated name slot. */
rz_type_db_format_set(typedb, "ut_anon", "x4.x4 a b");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_anon.";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
/* Two named fields: a and b. The skip slot is anonymous and
* dropped. */
mu_assert_eq(rz_pvector_len(&r->options), 2, "two named fields offered");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Two-level nested-struct descent: `outer` references `inner` via a
* STRUCT field, and `pf. outer.body.<TAB>` should descend into
* `inner`'s top-level fields. This is the smallest case that exercises
* the new re-parse-on-type_name loop in pf_resolve_path_format. */
static bool test_autocmplt_pf_format_path_nested(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_inner", "x4d4 first second");
rz_type_db_format_set(typedb, "ut_outer", "?(ut_inner) body");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_outer.body.";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 2, "inner's two fields offered");
mu_assert_eq(r->start, buf->length, "start advanced past the second dot");
bool saw_first = false, saw_second = false;
for (size_t i = 0; i < rz_pvector_len(&r->options); i++) {
const char *opt = rz_pvector_at(&r->options, i);
if (!strcmp(opt, "first")) {
saw_first = true;
}
if (!strcmp(opt, "second")) {
saw_second = true;
}
}
mu_assert_true(saw_first && saw_second, "first + second offered");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Three-level descent narrows correctly: `pf. A.B.C.bo<TAB>` should
* walk A -> B -> C and offer only fields of C that start with `bo`. */
static bool test_autocmplt_pf_format_path_three_levels(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_C", "x4d4d4 top bottom border");
rz_type_db_format_set(typedb, "ut_B", "?(ut_C) mid");
rz_type_db_format_set(typedb, "ut_A", "?(ut_B) outer");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_A.outer.mid.bo";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
/* Of C's fields (top, bottom, border) only the two starting with
* "bo" should be offered. */
mu_assert_eq(rz_pvector_len(&r->options), 2, "bottom + border match 'bo'");
bool saw_bottom = false, saw_border = false;
for (size_t i = 0; i < rz_pvector_len(&r->options); i++) {
const char *opt = rz_pvector_at(&r->options, i);
if (!strcmp(opt, "bottom")) {
saw_bottom = true;
}
if (!strcmp(opt, "border")) {
saw_border = true;
}
}
mu_assert_true(saw_bottom && saw_border, "bottom + border offered");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Array index in a middle segment: the cmd_pf2 test exercises
* `pf. troll.str[1].two` at runtime. Verify the completer accepts
* `troll.str[1].<TAB>` and offers plop's fields. */
static bool test_autocmplt_pf_format_path_array_index(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_plop", "d4x2x8x2 one two three four");
rz_type_db_format_set(typedb, "ut_troll", "[3]?(ut_plop) str");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_troll.str[1].";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 4, "plop's four fields offered");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* When the cursor sits inside an unclosed `[`, the user is mid-index
* and identifier completion would be nonsense; offer nothing. */
static bool test_autocmplt_pf_format_path_inside_brackets(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_arr", "[3]?(ut_arr_inner) arr");
rz_type_db_format_set(typedb, "ut_arr_inner", "x4 v");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_arr.arr[";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 0,
"no completion while cursor is mid-index");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* `name[N]` followed by no dot yet is also mid-descent: the next
* keystroke needs to be `.` before fields are meaningful, so offer
* nothing rather than suggesting field names that would be a syntax
* error if the user accepted them. */
static bool test_autocmplt_pf_format_path_after_close_bracket(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_acb_inner", "x4d4 a b");
rz_type_db_format_set(typedb, "ut_acb", "[3]?(ut_acb_inner) arr");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_acb.arr[2]";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 0,
"no completion after ] without trailing .");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
/* Descent through a scalar field is meaningless and must return no
* options (rather than offering top-level format names or crashing). */
static bool test_autocmplt_pf_format_path_through_scalar(void) {
RzCore *core = rz_core_new();
mu_assert_notnull(core, "core should be created");
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
rz_type_db_format_set(typedb, "ut_scalar_path", "x4d4 num val");
RzLineBuffer *buf = &core->cons->line->buffer;
const char *s = "pf. ut_scalar_path.num.";
strcpy(buf->data, s);
buf->length = strlen(s);
buf->index = buf->length;
RzLineNSCompletionResult *r = rz_core_autocomplete_rzshell(core, buf, RZ_LINE_PROMPT_DEFAULT);
mu_assert_notnull(r, "result");
mu_assert_eq(rz_pvector_len(&r->options), 0,
"no descent past a scalar field");
rz_line_ns_completion_result_free(r);
rz_core_free(core);
mu_end;
}
bool all_tests() {
mu_run_test(test_autocmplt_cmdid);
mu_run_test(test_autocmplt_newcommand);
mu_run_test(test_autocmplt_argid);
mu_run_test(test_autocmplt_quotedarg);
mu_run_test(test_autocmplt_newarg);
mu_run_test(test_autocmplt_arg_folder);
mu_run_test(test_autocmplt_fcn);
mu_run_test(test_autocmplt_eval);
mu_run_test(test_autocmplt_seek);
mu_run_test(test_autocmplt_global);
mu_run_test(test_autocmplt_tmp_operators);
mu_run_test(test_autocmplt_iter_operators);
mu_run_test(test_autocmplt_tmp_seek);
mu_run_test(test_autocmplt_tmp_config);
mu_run_test(test_autocmplt_tmp_arch);
mu_run_test(test_autocmplt_choices_cb_arg);
mu_run_test(test_autocmplt_eco_themes);
mu_run_test(test_autocmplt_pf_format_name);
mu_run_test(test_autocmplt_pf_format_path);
mu_run_test(test_autocmplt_pfw_format_path);
mu_run_test(test_autocmplt_pf_format_path_empty);
mu_run_test(test_autocmplt_pf_format_path_unknown_name);
mu_run_test(test_autocmplt_pf_format_path_anon_field);
mu_run_test(test_autocmplt_pf_format_path_nested);
mu_run_test(test_autocmplt_pf_format_path_three_levels);
mu_run_test(test_autocmplt_pf_format_path_array_index);
mu_run_test(test_autocmplt_pf_format_path_inside_brackets);
mu_run_test(test_autocmplt_pf_format_path_after_close_bracket);
mu_run_test(test_autocmplt_pf_format_path_through_scalar);
return tests_passed != tests_run;
}
mu_main(all_tests)