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:
parent
f2f4543a26
commit
d7aa7a664c
7 changed files with 756 additions and 45 deletions
38
doc/pf.md
38
doc/pf.md
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue