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).
This commit is contained in:
NOT XVilka 2026-06-02 13:44:07 +08:00 committed by GitHub
parent f2f4543a26
commit d7aa7a664c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 756 additions and 45 deletions

View file

@ -116,9 +116,17 @@ canonical form is `t(format)`.
- Inline: `B4(R=1,W=2,X=4)` -- flags declared inline in parens. The presence
of `=` inside the parens is the discriminator.
- `G[(layout)]` -- 16-byte GUID. Layouts:
- `ms` (default) -- Microsoft mixed-endian (D1 LE u32, D2/D3 LE u16, D4 raw)
- `be` -- RFC 4122 big-endian network order
- `le` -- all little-endian
- `ms` (default) -- Microsoft mixed-endian, as used by the Windows
GUID/UUID convention in COM, the registry, and most Win32 binary
formats: D1 LE u32, D2/D3 LE u16, D4 raw.
- `be` -- big-endian network order: D1/D2/D3 BE, D4 raw. This is the
UUID transmission form specified by
[RFC 4122](https://www.rfc-editor.org/rfc/rfc4122).
- `le` -- D1/D2/D3 little-endian; D4 stays in buffer order, so the
rendered string is identical to the default MS layout. The keyword
is accepted for callers that want to be explicit about endianness;
it does not reverse the D4 byte run and does not correspond to any
published standard.
- `V[(...)]` -- TLV record. Parameters in parens:
- `t=<type>` -- tag scalar (default `u1`)
- `l=<type>` -- length scalar (default `u2`)
@ -134,10 +142,22 @@ canonical form is `t(format)`.
prefix it consumes one byte.
- `U` / `L` -- ULEB128 / SLEB128 (variable-length unsigned / signed). Decoded
greedily up to 10 bytes (enough for a full 64-bit value).
- `n` / `N` -- context-sized integer:
- `n1`/`n2`/`n4`/`n8` -- forced byte width (1/2/4/8) in LE.
- `N1`/`N2`/`N4`/`N8` -- same, BE.
- Bare `n`/`N` defaults to `ctx.bits/8` bytes.
- `n` -- context-endian sized integer (unsigned hex):
- `n1`/`n2`/`n4`/`n8` -- 1/2/4/8 byte width. Endianness is taken
from the active pf parsing context (set by `pf -e le|be ...`, by
the format's own `e=...` directive on container specs like `V` or
`B`, or by the embedding API caller). Case of the spec letter is
*not* used to pin the endian, unlike `x{1,2,4,8}` (LE) and
`X{1,2,4,8}` (BE).
- The form is useful whenever the byte order isn't fixed by the
spec but determined at parse time -- common in file formats whose
headers carry an endian marker (ELF `EI_DATA`, MachO magic, the
GUID/UUID layout flag in some MS containers), but it also fits
serialized records, embedded protocols, and any structure where
the endianness lives outside the field itself.
- There is no uppercase `N` form and no bare `n` -- use a `1`/`2`/
`4`/`8` suffix or pick `x{1,2,4,8}` / `X{1,2,4,8}` when you do
want endian pinned by case.
### Pointers
@ -351,8 +371,8 @@ deprecation diagnostic:
| Legacy | Canonical | Meaning |
|--------|--------------|--------------------------------------|
| `b` | `b1` | 1-byte binary |
| `c` | `c` | 1-byte signed char (unchanged) |
| `d` | `d4` | 4-byte signed decimal LE |
| `x` | `x4` | 4-byte hex LE |
| `o` | `o4` | 4-byte octal LE |
| `q` | `x8` | 8-byte hex LE |
| `u` | `u4` | 4-byte unsigned decimal LE |
@ -361,7 +381,7 @@ deprecation diagnostic:
| `F` | `F8` | IEEE-754 double BE |
| `w` | `x2` | 2-byte hex LE |
| `Z` | `z(utf16le)` | UTF-16 LE zstring |
| `t` | `t(unix32)` | Unix-32 timestamp |
| `t` | `t(unix32)` | Unix-32 timestamp LE |
| `T` | `T(unix32)` | Unix-32 timestamp BE |
| `X` | `r` | raw hex byte dump |
| `C` | `u1` | 1-byte unsigned decimal |

View file

@ -5,6 +5,10 @@
#include <rz_core.h>
#include <rz_cons.h>
#include <rz_cmd.h>
#include <rz_pf.h>
#include <rz_util/rz_path.h>
#include <rz_util/rz_sys.h>
#include <rz_userconf.h>
/**
* Describe what needs to be autocompleted.
@ -435,6 +439,289 @@ static void autocmplt_cmd_arg_any_type(RzCore *core, RzLineNSCompletionResult *r
rz_list_free(list);
}
/* Offer the names of all `pf` named formats (those registered via `pfn`
* or seeded by `pfo`). Used by sub-commands that take a format-name
* argument: `pf-`, `pfn`, `pfs`, `pfv`. */
static void autocmplt_cmd_arg_pf_format_name(RzCore *core,
RzLineNSCompletionResult *res, const char *s, size_t len) {
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
RzList *list = rz_type_db_format_all(typedb);
if (!list) {
return;
}
RzListIter *iter;
RzTypeFormat *fmt;
rz_list_foreach (list, iter, fmt) {
if (fmt->name && !strncmp(fmt->name, s, len)) {
rz_line_ns_completion_result_add(res, fmt->name);
}
}
rz_list_free(list);
}
/* Helper: consume one path segment of the form `name`, optionally
* followed by `[N]` and optionally followed by `.`. \p in points at
* the first character of the segment, \p end is the hard read limit.
*
* Concretely, for the four shapes the function accepts:
*
* "field" -> consumed = 5, *out_name_len = 5, *out_has_idx = false
* "field." -> consumed = 6, *out_name_len = 5, *out_has_idx = false
* "field[3]" -> consumed = 8, *out_name_len = 5, *out_has_idx = true
* "field[3]." -> consumed = 9, *out_name_len = 5, *out_has_idx = true
*
* Returns the byte count consumed (including the optional `[N]` and
* the trailing `.` if present). Returns 0 when the segment is
* malformed in a way that should stop the walk (empty name, an
* unterminated `[`, an empty `[]`, or a non-numeric index). \p end
* is a hard stop: parsing never reads past it.
*
* Hand-rolled rather than regex: the segment grammar is small
* enough that a single forward pass without backtracking is shorter
* than calling into RzRegex + submatch extraction, and it stays
* zero-allocation in the completion hot path. */
static size_t pf_path_seg_consume(const char *in, const char *end,
size_t *out_name_len, bool *out_has_idx) {
const char *name_end = in;
while (name_end < end && *name_end != '.' && *name_end != '[') {
name_end++;
}
if (name_end == in) {
return 0; /* empty name */
}
*out_name_len = name_end - in;
*out_has_idx = false;
const char *cursor = name_end;
if (cursor < end && *cursor == '[') {
const char *digits = cursor + 1;
const char *rb = digits;
while (rb < end && *rb != ']') {
if (*rb < '0' || *rb > '9') {
return 0;
}
rb++;
}
if (rb >= end || *rb != ']') {
return 0; /* unterminated [ */
}
if (rb == digits) {
return 0; /* empty [] */
}
*out_has_idx = true;
cursor = rb + 1;
}
if (cursor < end && *cursor == '.') {
cursor++;
}
return cursor - in;
}
/* Helper: walk \p committed and return the RzPfFormat whose fields
* the caller should complete against, or NULL if descent isn't
* possible (unknown name, non-struct field, malformed segment).
*
* \p committed is the prefix of the user's input up to and including
* the last `.` -- i.e. every fully-typed segment. The walk consumes
* one segment per loop iteration (via \ref pf_path_seg_consume), looks
* the segment's name up in the current format, and re-parses the
* named struct's body when a `type_name` is present.
*
* Same rationale as \ref pf_path_seg_consume for not using regex: the
* loop is a thin glue layer over pf_path_seg_consume + a typedb
* lookup, with no extraction step that a regex would simplify. */
static RZ_OWN RzPfFormat *pf_resolve_path_format(RzTypeDB *typedb,
const char *committed, size_t committed_len) {
const char *end = committed + committed_len;
const char *p = committed;
RzPfFormat *cur = NULL;
/* First segment: must resolve through the typedb (top-level
* format name). */
size_t name_len = 0;
bool has_idx = false;
size_t consumed = pf_path_seg_consume(p, end, &name_len, &has_idx);
if (!consumed) {
return NULL;
}
char *name = rz_str_ndup(p, name_len);
if (!name) {
return NULL;
}
const char *body = rz_type_db_format_get(typedb, name);
free(name);
if (RZ_STR_ISEMPTY(body)) {
return NULL;
}
cur = rz_pf_parse(body);
if (!cur) {
return NULL;
}
p += consumed;
/* Subsequent segments: each must name a STRUCT field within the
* current format whose `type_name` resolves to another named
* format. Scalars and inline structs (no type_name) cannot be
* descended into. */
while (p < end) {
consumed = pf_path_seg_consume(p, end, &name_len, &has_idx);
if (!consumed) {
goto err;
}
const RzPfField *match = NULL;
for (int i = 0; i < cur->nfields; i++) {
const char *fname = cur->fields[i].name;
if (fname && strlen(fname) == name_len &&
!strncmp(fname, p, name_len)) {
match = &cur->fields[i];
break;
}
}
if (!match || match->type != RZ_PF_STRUCT ||
RZ_STR_ISEMPTY(match->type_name)) {
goto err;
}
const char *child_body = rz_type_db_format_get(typedb,
match->type_name);
if (RZ_STR_ISEMPTY(child_body)) {
goto err;
}
RzPfFormat *child = rz_pf_parse(child_body);
if (!child) {
goto err;
}
rz_pf_format_free(cur);
cur = child;
p += consumed;
}
return cur;
err:
rz_pf_format_free(cur);
return NULL;
}
/* Offer a format name optionally followed by a dotted path of fields,
* with optional `[N]` array indices on any segment. Used by `pf.` and
* `pfw`, both of which accept `name[.field[N]?]*`.
*
* The path scan -- both the last-dot search done here and the segment
* walk delegated to \ref pf_path_seg_consume / \ref
* pf_resolve_path_format -- is hand-rolled rather than regex-driven:
* single forward / backward passes over a short buffer with no
* capture-group extraction, called on every TAB. See
* \ref pf_path_seg_consume for the shape of one segment.
*
* UX rules:
* - No `.` in the partial -> complete the top-level format name and
* suppress the trailing space so `.` can be typed next without
* space insertion.
* - `.` is the segment separator. The cursor sits in the final
* segment; everything before the last `.` is the committed path.
* - Inside `[...]` or immediately after `]` (no trailing `.`), the
* user is mid-index, not mid-identifier: nothing useful to offer.
* - The committed path is walked through STRUCT-typed children
* (whose `type_name` resolves to another registered format).
* Walking past a scalar or an inline struct returns nothing. */
static void autocmplt_cmd_arg_pf_format_path(RzCore *core,
RzLineNSCompletionResult *res, const char *s, size_t len) {
/* Find the LAST dot in the partial input. Everything before it
* (inclusive) is the committed path; what follows is the segment
* we're completing. NOTE: rz_sub_str_rchr returns the FIRST
* match within its range (the `r` is for "range", not "right"),
* so we have to walk manually. */
const char *last_dot = NULL;
for (size_t i = len; i > 0; i--) {
if (s[i - 1] == '.') {
last_dot = s + (i - 1);
break;
}
}
if (!last_dot) {
/* No dot yet: complete the format name. Leave `end_string`
* empty so the cursor stays right after the name and the
* user can type `.` to descend into fields without an
* intervening space being inserted. */
res->end_string = "";
autocmplt_cmd_arg_pf_format_name(core, res, s, len);
return;
}
/* The tail (post-last-dot) is what we complete. If the tail
* contains `[`, the cursor is mid-index; we offer nothing. A
* trailing `]` with no following `.` is also mid-descent: the
* user still needs to type the dot before fields are
* meaningful. */
const char *tail = last_dot + 1;
size_t tail_len = len - (tail - s);
if (memchr(tail, '[', tail_len) || memchr(tail, ']', tail_len)) {
return;
}
RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis);
/* Walk every segment up to and including the last `.`. */
size_t committed_len = (last_dot - s) + 1;
RzPfFormat *parsed = pf_resolve_path_format(typedb, s, committed_len);
if (!parsed) {
return;
}
/* Rewrite `start` so the completion only replaces the tail; the
* committed path stays put. End-string stays empty so further
* descent (typing the next `.`) doesn't get a space jammed in. */
res->start += (tail - s);
res->end_string = "";
for (int i = 0; i < parsed->nfields; i++) {
const char *fname = parsed->fields[i].name;
if (fname && !strncmp(fname, tail, tail_len)) {
rz_line_ns_completion_result_add(res, fname);
}
}
rz_pf_format_free(parsed);
}
/* List the basenames of Format Definition Files (.fdf and other format
* scripts) found in the user's home FDF directory and the system FDF
* directory. Mirrors the search order used by `pfo`, including the
* HtSU-based dedup for files installed in both locations. */
static void autocmplt_cmd_arg_pf_fdf_file(RzCore *core,
RzLineNSCompletionResult *res, const char *s, size_t len) {
HtSU *seen = ht_su_new(HT_STR_DUP);
char *home = rz_path_home_prefix(RZ_SDB_FORMAT);
char *sysdir = rz_path_system(core->sys_path, RZ_SDB_FORMAT);
const char *dirs[2] = { home, sysdir };
for (int i = 0; i < 2; i++) {
if (!dirs[i]) {
continue;
}
RzList *files = rz_sys_dir(dirs[i]);
if (!files) {
continue;
}
RzListIter *iter;
const char *fn;
rz_list_foreach (files, iter, fn) {
/* Skip dotfiles (`.`, `..`, and hidden files: FDF
* conventions don't use leading dots). */
if (!*fn || *fn == '.') {
continue;
}
if (seen && ht_su_find(seen, fn, NULL)) {
continue;
}
if (!strncmp(fn, s, len)) {
rz_line_ns_completion_result_add(res, fn);
}
if (seen) {
ht_su_insert(seen, fn, 1);
}
}
rz_list_free(files);
}
free(home);
free(sysdir);
ht_su_free(seen);
}
static void autocmplt_cmd_arg_global_var(RzCore *core, RzLineNSCompletionResult *res, const char *s, size_t len) {
RzAnalysisVarGlobal *glob;
RzListIter *iter;
@ -764,6 +1051,16 @@ static void autocmplt_cmd_arg(RzCore *core, RzLineNSCompletionResult *res, const
break;
case RZ_CMD_ARG_TYPE_FOLDER:
autocmplt_cmd_arg_folder(res, s, len);
break;
case RZ_CMD_ARG_TYPE_PF_FORMAT_NAME:
autocmplt_cmd_arg_pf_format_name(core, res, s, len);
break;
case RZ_CMD_ARG_TYPE_PF_FORMAT_PATH:
autocmplt_cmd_arg_pf_format_path(core, res, s, len);
break;
case RZ_CMD_ARG_TYPE_PF_FDF_FILE:
autocmplt_cmd_arg_pf_fdf_file(core, res, s, len);
break;
default:
break;
}

View file

@ -15988,14 +15988,14 @@ static const RzCmdDescDetailEntry pf_Sized_space_integers_space__oparen_lowercas
{ .text = "u1 / u2 / u4 / u8", .arg_str = NULL, .comment = "decimal unsigned, N bytes" },
{ .text = "o1 / o2 / o4 / o8", .arg_str = NULL, .comment = "octal, N bytes" },
{ .text = "b1 / b2 / b4 / b8", .arg_str = NULL, .comment = "binary, N bytes" },
{ .text = "n1 / n2 / n4 / n8", .arg_str = NULL, .comment = "hex unsigned, N bytes, context-endian (follows ctx.big_endian; for headers like ELF that pick endian via a data byte)" },
{ .text = "n1 / n2 / n4 / n8", .arg_str = NULL, .comment = "hex unsigned, N bytes; endian comes from the active pf parsing context (set by pf -e, by a parent V/B's e=..., or by the embedding caller), not from the spec's case; useful when the byte order isn't fixed in the format itself (file headers with an endian marker, embedded protocols, serialised records)" },
{ .text = "f2 / f4 / f8", .arg_str = NULL, .comment = "IEEE 754 float, 2/4/8 bytes (half/single/double)" },
{ 0 },
};
static const RzCmdDescDetailEntry pf_Special_space_scalars_detail_entries[] = {
{ .text = "c", .arg_str = NULL, .comment = "single byte rendered as character" },
{ .text = "p", .arg_str = NULL, .comment = "pointer (size from ctx.bits: 2 / 4 / 8 bytes)" },
{ .text = "p / p2 / p4 / p8", .arg_str = NULL, .comment = "pointer: bare p uses ctx.bits (2/4/8 bytes); p2/p4/p8 force the width" },
{ .text = "Q", .arg_str = NULL, .comment = "uint128_t (16 bytes, byte-sequential)" },
{ .text = "r", .arg_str = NULL, .comment = "raw hex byte dump (count via [N])" },
{ .text = "U / L", .arg_str = NULL, .comment = "ULEB128 / SLEB128 (variable length)" },
@ -16031,8 +16031,9 @@ static const RzCmdDescDetailEntry pf_DSL_space_extensions_detail_entries[] = {
{ .text = ":N", .arg_str = NULL, .comment = "read N bits (1..64) from packed bitstream; MSB-first by default" },
{ .text = ":N< / :N>", .arg_str = NULL, .comment = "explicit bit order: < = LSB-first, > = MSB-first" },
{ .text = "G", .arg_str = NULL, .comment = "16-byte GUID/UUID, mixed-endian (MS) layout by default" },
{ .text = "G(le) / G(be)", .arg_str = NULL, .comment = "GUID layout: all-LE or RFC 4122 BE" },
{ .text = "V(t=u1,l=u2,d=table)", .arg_str = NULL, .comment = "TLV record; t=tag, l=length, e=le/be, h=v/a (header inclusion), d=dispatch table" },
{ .text = "G(le) / G(be)", .arg_str = NULL, .comment = "GUID layout: G(le) is LE on D1/D2/D3 (D4 stays raw, effectively the MS layout); G(be) is RFC 4122 BE" },
{ .text = "V(t=u1,l=u2,d=table)", .arg_str = NULL, .comment = "TLV record; t=tag, l=length, e=le/be, h=v/l/a (len covers value / len+value / tag+len+value), d=dispatch table" },
{ .text = "v(N) / v(N,lsb) / v(N,msb)", .arg_str = NULL, .comment = "bitvector: N individual bits (1..4096) exposed as separate 0/1 scalars; consumes ceil(N/8) bytes; bytes are always shown low-address to high; within each byte the default (msb) prints bit 7 first through bit 0, while lsb flips that to bit 0 first through bit 7" },
{ .text = "[@field_name]T", .arg_str = NULL, .comment = "array whose length comes from an earlier scalar field" },
{ 0 },
};
@ -16063,9 +16064,9 @@ static const RzCmdDescDetailEntry pf_Examples_detail_entries[] = {
static const RzCmdDescDetailEntry pf_Notes_detail_entries[] = {
{ .text = "bit order", .arg_str = NULL, .comment = ":N defaults to MSB-first (matches DWARF and most network protocols); use <N for LSB-first" },
{ .text = "endian via case", .arg_str = NULL, .comment = "lowercase specifier = little-endian, UPPERCASE = big-endian" },
{ .text = "context endian", .arg_str = NULL, .comment = "n1/n2/n4/n8 follow the surrounding RzPfCtx.big_endian flag instead of being pinned by case" },
{ .text = "context endian", .arg_str = NULL, .comment = "n1/n2/n4/n8 take the active pf parsing context's current endian setting, not the spec's case" },
{ .text = "skip vs alignment", .arg_str = NULL, .comment = "'.' / '[N].' skip an exact number of bytes; '@N' pads to the next N-byte boundary" },
{ .text = "deprecation", .arg_str = NULL, .comment = "bare-letter codes (b, c, d, f, o, q, s, t, u, w, x, z) still parse with a warning; use sized forms in new code" },
{ .text = "deprecation", .arg_str = NULL, .comment = "bare-letter codes (b, C, d, f, F, i, o, q, t, T, w, x, X, Z) still parse with a one-time warning; use the sized or parenthesised forms in new code" },
{ .text = "pf 'X2D4u8 bigWord beef qword'", .arg_str = NULL, .comment = "BE u16, BE s32, LE u64 -- one of every endianness/sign combination" },
{ .text = "pfn foo 'rr (eax)reg1 (eip)reg2'", .arg_str = NULL, .comment = "Create object foo referencing two registers" },
{ .text = "pf 't(unix32)t(unix32) troll plop'", .arg_str = NULL, .comment = "Print two unix-epoch (32-bit) timestamps with labels 'troll' and 'plop'" },
@ -16104,7 +16105,7 @@ static const RzCmdDescHelp cmd_print_format_help = {
static const RzCmdDescArg cmd_print_format_delete_args[] = {
{
.name = "formatname",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16126,7 +16127,7 @@ static const RzCmdDescHelp cmd_print_format_delete_all_help = {
static const RzCmdDescArg cmd_print_format_apply_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16140,7 +16141,7 @@ static const RzCmdDescHelp cmd_print_format_apply_help = {
static const RzCmdDescArg cmd_print_format_c_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16154,7 +16155,7 @@ static const RzCmdDescHelp cmd_print_format_c_help = {
static const RzCmdDescArg cmd_print_format_dot_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16168,7 +16169,7 @@ static const RzCmdDescHelp cmd_print_format_dot_help = {
static const RzCmdDescArg cmd_print_format_named_dot_args[] = {
{
.name = "formatname",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_PATH,
.flags = RZ_CMD_ARG_FLAG_LAST,
.optional = true,
@ -16183,7 +16184,7 @@ static const RzCmdDescHelp cmd_print_format_named_dot_help = {
static const RzCmdDescArg cmd_print_format_named_args[] = {
{
.name = "formatname",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.optional = true,
},
@ -16204,7 +16205,7 @@ static const RzCmdDescHelp cmd_print_format_named_help = {
static const RzCmdDescArg cmd_print_format_file_args[] = {
{
.name = "file",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FDF_FILE,
.flags = RZ_CMD_ARG_FLAG_LAST,
.optional = true,
@ -16219,7 +16220,7 @@ static const RzCmdDescHelp cmd_print_format_file_help = {
static const RzCmdDescArg cmd_print_format_size_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16233,7 +16234,7 @@ static const RzCmdDescHelp cmd_print_format_size_help = {
static const RzCmdDescArg cmd_print_format_value_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_NAME,
.flags = RZ_CMD_ARG_FLAG_LAST,
},
@ -16247,7 +16248,7 @@ static const RzCmdDescHelp cmd_print_format_value_help = {
static const RzCmdDescArg cmd_print_format_write_args[] = {
{
.name = "format",
.type = RZ_CMD_ARG_TYPE_STRING,
.type = RZ_CMD_ARG_TYPE_PF_FORMAT_PATH,
},
{

View file

@ -22,6 +22,9 @@ CD_ARG_LAST_TYPES = [
"RZ_CMD_ARG_TYPE_RZNUM",
"RZ_CMD_ARG_TYPE_STRING",
"RZ_CMD_ARG_TYPE_CMD",
"RZ_CMD_ARG_TYPE_PF_FORMAT_NAME",
"RZ_CMD_ARG_TYPE_PF_FORMAT_PATH",
"RZ_CMD_ARG_TYPE_PF_FDF_FILE",
]

View file

@ -479,7 +479,7 @@ commands:
cname: cmd_print_format_delete
args:
- name: formatname
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pf-*
summary: Remove all named formats
cname: cmd_print_format_delete_all
@ -489,19 +489,19 @@ commands:
cname: cmd_print_format_apply
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pfc
summary: Show data using given format string with C syntax
cname: cmd_print_format_c
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pfd
summary: Show data using given format string as DOT
cname: cmd_print_format_dot
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pf.
summary: Show data using given named format
cname: cmd_print_format_named_dot
@ -512,14 +512,14 @@ commands:
- RZ_OUTPUT_MODE_QUIET
args:
- name: formatname
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_PATH
optional: true
- name: pfn
summary: List named formats/Print named format string/Define a new named format
cname: cmd_print_format_named
args:
- name: formatname
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
optional: true
- name: formatstring
type: RZ_CMD_ARG_TYPE_STRING
@ -529,26 +529,26 @@ commands:
cname: cmd_print_format_file
args:
- name: file
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FDF_FILE
optional: true
- name: pfs
summary: Print the size of format in bytes
cname: cmd_print_format_size
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pfv
summary: Print the value for named format
cname: cmd_print_format_value
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_NAME
- name: pfw
summary: Write data using given format string
cname: cmd_print_format_write
args:
- name: format
type: RZ_CMD_ARG_TYPE_STRING
type: RZ_CMD_ARG_TYPE_PF_FORMAT_PATH
- name: value
type: RZ_CMD_ARG_TYPE_STRING
optional: true
@ -567,17 +567,20 @@ commands:
comment: "binary, N bytes"
- text: "n1 / n2 / n4 / n8"
comment: >
hex unsigned, N bytes, context-endian (follows
ctx.big_endian; for headers like ELF that pick endian
via a data byte)
hex unsigned, N bytes; endian comes from the active pf
parsing context (set by pf -e, by a parent V/B's e=...,
or by the embedding caller), not from the spec's case;
useful when the byte order isn't fixed in the format
itself (file headers with an endian marker, embedded
protocols, serialised records)
- text: "f2 / f4 / f8"
comment: "IEEE 754 float, 2/4/8 bytes (half/single/double)"
- name: Special scalars
entries:
- text: "c"
comment: "single byte rendered as character"
- text: "p"
comment: "pointer (size from ctx.bits: 2 / 4 / 8 bytes)"
- text: "p / p2 / p4 / p8"
comment: "pointer: bare p uses ctx.bits (2/4/8 bytes); p2/p4/p8 force the width"
- text: "Q"
comment: "uint128_t (16 bytes, byte-sequential)"
- text: "r"
@ -625,9 +628,21 @@ commands:
- text: "G"
comment: "16-byte GUID/UUID, mixed-endian (MS) layout by default"
- text: "G(le) / G(be)"
comment: "GUID layout: all-LE or RFC 4122 BE"
comment: >
GUID layout: G(le) is LE on D1/D2/D3 (D4 stays raw,
effectively the MS layout); G(be) is RFC 4122 BE
- text: "V(t=u1,l=u2,d=table)"
comment: "TLV record; t=tag, l=length, e=le/be, h=v/a (header inclusion), d=dispatch table"
comment: >
TLV record; t=tag, l=length, e=le/be, h=v/l/a (len
covers value / len+value / tag+len+value), d=dispatch
table
- text: "v(N) / v(N,lsb) / v(N,msb)"
comment: >
bitvector: N individual bits (1..4096) exposed as
separate 0/1 scalars; consumes ceil(N/8) bytes; bytes
are always shown low-address to high; within each byte
the default (msb) prints bit 7 first through bit 0,
while lsb flips that to bit 0 first through bit 7
- text: "[@field_name]T"
comment: "array whose length comes from an earlier scalar field"
- name: Skip / repeat / pointers
@ -671,13 +686,14 @@ commands:
- text: "endian via case"
comment: "lowercase specifier = little-endian, UPPERCASE = big-endian"
- text: "context endian"
comment: "n1/n2/n4/n8 follow the surrounding RzPfCtx.big_endian flag instead of being pinned by case"
comment: "n1/n2/n4/n8 take the active pf parsing context's current endian setting, not the spec's case"
- text: "skip vs alignment"
comment: "'.' / '[N].' skip an exact number of bytes; '@N' pads to the next N-byte boundary"
- text: "deprecation"
comment: >
bare-letter codes (b, c, d, f, o, q, s, t, u, w, x, z)
still parse with a warning; use sized forms in new code
bare-letter codes (b, C, d, f, F, i, o, q, t, T, w, x,
X, Z) still parse with a one-time warning; use the
sized or parenthesised forms in new code
- text: "pf 'X2D4u8 bigWord beef qword'"
comment: "BE u16, BE s32, LE u64 -- one of every endianness/sign combination"
- text: "pfn foo 'rr (eax)reg1 (eip)reg2'"

View file

@ -54,6 +54,9 @@ typedef enum rz_cmd_arg_type_t {
RZ_CMD_ARG_TYPE_REG_FILTER, ///< Argument is a register name, size, type or "all"
RZ_CMD_ARG_TYPE_REG_TYPE, ///< Argument is a register type/arena like "gpr"
RZ_CMD_ARG_TYPE_FOLDER, ///< Argument is a directory or path
RZ_CMD_ARG_TYPE_PF_FORMAT_NAME, ///< Argument is the name of a `pf` named format (registered via `pfn`)
RZ_CMD_ARG_TYPE_PF_FORMAT_PATH, ///< Argument is a `pf` named format name optionally followed by `.<field>` (for `pf.` and `pfw`)
RZ_CMD_ARG_TYPE_PF_FDF_FILE, ///< Argument is the name of a Format Definition File (.fdf), looked up in the FDF search dirs
} RzCmdArgType;
/**

View file

@ -783,6 +783,365 @@ static bool test_autocmplt_eco_themes(void) {
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);
@ -801,6 +1160,18 @@ bool all_tests() {
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;
}