When type_to_format / type_to_format_pair encounter a pointer field whose
pointee is reachable only through a typedef chain that ends at the
atomic 'void' or 'char' (e.g. PVOID -> VOID -> void, LPSTR -> CHAR ->
char, HANDLE -> ... -> void in some platform headers), they emit a bare
'*' and recurse into the pointee. The recursion produces nothing for
'void' (no format) and emits the legacy 'c' for 'char', so the
generated pf string ends up with either an orphan trailing/internal '*'
or the sequence '*c', which the new pf parser introduced in the
recent rewrite rejects: it requires '*' to be followed by a complete
dereferenceable spec ('z', a sized integer like 'd4'/'x2'/'u8', or
'?'). The result is parser warnings and dropped fields during 'tp':
rizin -k windows -c 'tp _OBJECT_ATTRIBUTES'
WARNING: pf: unknown specifier '*' at position 4, skipping
WARNING: pf: unknown specifier '*' at position 4, skipping
[only 4 of 6 fields rendered]
rizin -k windows -c 'tp _SYSTEM_INFO'
[11 fields collapse to 4]
rizin -k windows -c 'tp _STARTUPINFOA'
[three LPSTR fields render as '*c' which the parser cannot
usefully follow]
The top-level rz_type_as_format() already special-cases 'void *',
'char *', and callable pointers, mapping them to 'p' and 'z'. But the
inner walkers do not, because rz_type_is_void_ptr / rz_type_is_char_ptr
compare the literal identifier name and so do not see through typedefs
like VOID->void or CHAR->char.
Fix it by adding ptr_pointee_resolves_to(), a small static helper in
format.c that walks the typedb to find the canonical atomic name for
the pointee (bounded depth so a circular typedef cannot send the
resolver into an infinite loop), and using it in both POINTER branches
before the '*'+recurse fallback. Pointers whose pointee resolves to
'void' (or a void-aliased typedef) now emit 'p'; pointers whose pointee
resolves to 'char' (or a char-aliased typedef) emit 'z'. Everything
else continues to emit '*<inner>' unchanged, so single-level
LPBYTE -> BYTE -> unsigned char still renders as the perfectly valid
'*x1', and pointer-to-struct '*?' chains are untouched.
After the fix, the same upstream structs produce well-formed pf
strings:
ts _OBJECT_ATTRIBUTES -> 'x8p**x1x8pp ...' (two PVOID -> pp)
ts _SYSTEM_INFO -> 'x2x2d4ppx8d4d4d4x2x2 ...' (two LPVOID -> pp)
ts _STARTUPINFOA -> 'd4zzzd4...*x1ppp ...' (three LPSTR -> zzz;
LPBYTE still '*x1')
Add a regression test in test/db/cmd/cmd_pf that exercises both shapes
via 'ts _SECURITY_ATTRIBUTES' (single LPVOID) and 'ts _STARTUPINFOA'
(three LPSTR + one LPBYTE). Without this commit the test catches the
bug -- the LPVOID field becomes orphan '*' inside the format and the
LPSTR fields render as '*c'; with the commit both render cleanly as
documented above.
Also update one existing EXPECT in test/db/cmd/types: the 'td with
comments' test had encoded the legacy 'pf "d4[5]*c b foo"' shape for
'char *foo[5]'. With the fix this becomes 'pf "d4[5]z b foo"', which
is both well-formed under the new DSL and a more accurate description
(an array of zstrings rather than an array of pointers to a single
char).
Co-authored-by: Anton Kochkov <anton.kockov@gmail.com>
|
||
|---|---|---|
| .. | ||
| parser | ||
| pf | ||
| base.c | ||
| format.c | ||
| function.c | ||
| helpers.c | ||
| meson.build | ||
| path.c | ||
| README.md | ||
| serialize_functions.c | ||
| serialize_types.c | ||
| type.c | ||
| typeclass.c | ||
Type database and pf format engine
This library owns Rizin's representation of C-style types (structs, unions,
enums, typedefs, callables) and the pf print-format language for decoding
raw bytes through that type information.
Public headers
| Header | Surface |
|---|---|
<rz_type.h> |
RzTypeDB, base types, callables, format storage |
<rz_pf.h> |
RzPfFormat, RzPfValue, RzPfCtx, parser/reader/renderer |
<rz_pf.h> is included transitively from <rz_type.h>, so callers that
already include the latter need not include the former explicitly.
File map
base.c RzBaseType (struct/union/enum/typedef) lifecycle
format.c Format-string codegen from RzType; named-format storage
function.c RzCallable definitions (function prototypes)
helpers.c Type comparison, size, attribute helpers
path.c Path-style access into nested types
serialize_*.c Project (de)serialization of types and functions
parser/ Tree-sitter based C type parser
pf/ The pf (print-format) engine. All sources live
under this directory; only format.c stays at the
librz/type/ top level.
pf/pf_parser.c Parse driver, type-spec dispatcher, reader core,
parsing context, public utility surface
pf/pf_parser.h Internal alias header -- includes <rz_pf.h>, plus
helpers shared with pf_render.c (is_string_type,
is_raw_type, endian_str, pf_vasprintf) as inlines
pf/pf_internal.h Cross-TU glue: PF_DIAG diagnostic macro, the
shared ReadState, and declarations of every
function that crosses a pf file boundary
pf/pf_parser_string.c String/encoding spec parsing + reading (z / s / Z)
pf/pf_parser_bitfield.c Inline + typed bitfield parsing (B...)
pf/pf_parser_bitvec.c Bitvector parsing + reading (v(N))
pf/pf_parser_array.c Array-count resolution ([N] / [@name])
pf/pf_parser_struct.c Nested struct / union reading (?)
pf/pf_parser_time.c Timestamp wire-format decoders
(filetime, dos, hfs, oletime, webkit, cocoa, ...)
pf/pf_parser_time.h Private API between pf_parser.c and pf_parser_time.c
pf/pf_parser_tlv.c TLV (Tag-Length-Value) record parsing and dispatch
pf/pf_render.c Shared render helpers (pf_field_matches,
pf_scalar_text, pf_render_guid) and the
rz_pf_render() mode dispatcher
pf/pf_render.h Cross-TU glue for the renderers: the RenderCtx
record and shared-helper / per-mode declarations
pf/pf_render_text.c Text + quiet renderers
pf/pf_render_json.c JSON renderer (+ rz_pf_render_json entry point)
pf/pf_render_cstruct.c C-struct renderer
pf/pf_render_dot.c Graphviz DOT renderer
pf/pf_render_sd.c RzStructuredData renderer (rz_pf_render_sd)
pf architecture
The pf format engine is a three-stage pipeline. Each stage is a public API
entry point so callers can mix and match:
parse read render
+--------+ +------------------+ +---------------------+
| source | --> | format -> values | --> | values -> string |
| string | | (RzPfFormat, | | (text / json / |
| | | RzPfValue[]) | | cstruct / quiet / |
+--------+ +------------------+ | dot / write) |
+---------------------+
| Stage | Entry point | Inputs | Output |
|---|---|---|---|
| parse | rz_pf_parse |
format string | RzPfFormat * |
| read | rz_pf_read |
format, buffer, base addr, ctx | RzPfValue * |
| render | rz_pf_render |
values, count, mode, opts | char * |
| all-in-one | rz_pf_format |
source + buf + ctx + mode + opts | char * |
parse
The parser is a single-pass recursive-descent walker over the format string. For each token it decides the field type and any per-field metadata (endianness, array count, encoding, timestamp format, GUID layout, TLV spec, etc.). The token shape is the source of truth -- there is no separate lexer.
Diagnostics are recorded both on the RzPfFormat::errors[] array (with
positioned messages) and on RZ_LOG_WARN (for backward compatibility). The
parser uses two file-scoped pointers (g_current_fmt, g_current_src) to
let leaf helpers emit positioned diagnostics without threading a context
pointer through every signature; these are set on entry and restored on
exit so the parser is safe to call recursively from read_nested_struct.
TLV specs (V(t=...,l=...,e=...,h=...,d=...)) and TLV value reads live in
pf_parser_tlv.c. The two TUs cross-call through a small set of internal
helpers (pf_emit_error, pf_current_fmt, pf_current_src,
pf_parse_tlv_spec, pf_tlv_read) declared extern in pf_parser_tlv.c
and defined in pf_parser.c.
read
The reader walks the parsed format over a byte buffer and produces one
RzPfValue per top-level field, with nested struct children attached. The
read pass uses a per-instance ReadState:
typedef struct {
int bit_cursor; /* 0..7 for :N bit fields */
const RzPfValue *siblings; /* for [@name] lookups */
int n_siblings;
} ReadState;
The bit cursor is scoped to a single top-level repetition and a single
nested-struct instance. When a non-bit field is read while
bit_cursor != 0, the cursor snaps to the next byte boundary; this is the
"snap-flush" rule.
Bitvector fields (v(N)) intentionally bypass the bit cursor: each
v(N) reads exactly ceil(N/8) whole bytes and unpacks them into
N individual 0/1 scalars. They do not pack with neighbouring :N
fields, and reading a v(N) field next to a :N field will flush
any partially-consumed bit cursor first. The bit-order knob (lsb
vs msb) governs the per-byte unpacking, not the byte order itself.
[@name] length-by-reference works by scanning siblings[0..n_siblings)
for an earlier field with the matching name. Lookups never cross struct
boundaries.
Pointer dereference is delegated to the caller via RzPfCtx::read_at so
the type subsystem stays I/O-agnostic; the disasm and core printers
forward to rz_io_nread_at.
render
Rendering is a separate pass over the RzPfValue[]. Most modes produce a
string; WRITE is handled by the bridge in librz/core rather than
rz_pf_render, and the structured-data renderer returns a tree rather
than text (see below):
- text --
<offset> <name> : [endian] <value>one line per field. Optionally colorised viaRzPfRenderOpts::palette; the palette holds ANSI-escape strings indexed by role (offset / name / endian / hex_literal / label / reset). - json -- JSON array (via
PJ), with nested struct children under"fields". - cstruct -- C
struct[ <name>] { ... }mirror with decoded values in comments. The format name (when invoked aspfc <name>) follows thestructkeyword; anonymous formats render asstruct { ... }. - quiet -- value-only, no offsets or names.
- dot -- Graphviz
digraphrecord node with four column-aligned rows (offset / type-glyph / name / value), one column per visible field. The graph label is taken fromRzPfRenderOpts::graph_label. - structured data -- not a string mode:
rz_pf_render_sd()returns anRzStructuredDatatree (the generic key/value document model shared withrz_bin, ASN.1 and PKCS#7), which the caller can serialise to JSON or YAML or walk with the generic iterator.
RzPfRenderOpts additionally holds an optional field_filter (skip
fields whose name does not match) used by pf.<name>.<field> selectors.
The renderer dispatcher is rz_pf_render() in pf_render.c, which fans
out to one translation unit per mode (pf_render_text.c,
pf_render_json.c, pf_render_cstruct.c, pf_render_dot.c,
pf_render_sd.c). pf_render.c keeps only the dispatcher and the helpers
shared across modes (pf_field_matches, pf_scalar_text,
pf_render_guid); pf_render.h declares those plus the shared
RenderCtx record and the per-mode entry points.
Typedb integration
Named formats are stored in RzTypeDB::formats (HtSS). rz_pf_resolve_name
is the canonical resolver and the only function inside the type subsystem
that callers should use to look up a format by name. The TLV dispatch table
uses a tlv.<name>.<hex-tag> key convention in the same hash.
When a format references a typename via ?(Name) / E (Name) f /
B (Name) f, the reader consults typedb->types (for enums and bitfields)
or recursively rz_pf_resolve_name + rz_pf_parse (for nested struct
formats). Recursion is bounded by RzPfCtx::max_depth (default 32) to
catch self-referential structs cleanly.
Error reporting
Each RzPfError carries:
severity:RZ_PF_ERR_WARN(recoverable) orRZ_PF_ERR_ERROR(structural problem)category:SYNTAX,SEMANTIC,RANGE,DEPRECATED,DATA,DEPTHpos: 0-based column into the source format stringmessage: human-readable diagnostic
rz_pf_format_errors_to_string() renders the array with caret-position
lines pointing at the offending column.
Reference
See doc/pf.md for the user-facing pf DSL reference.