The C grammar already exposes a struct/union member's bitfield width as a
"bitfield_clause" node and the parser already recognized "int a : 4;", but
it threw the width away (the member was stored with size 0 and a FIXME). As
a result "tc" rendered the member as a plain "int a;", "ts" produced a
full-int "pf" format ("pf d4d4d4 a b c") and "tp" mis-read every field as a
whole integer.
RzTypeStructMember and RzTypeUnionMember already carry a "size" field
documented as "in bits"; it is now used as the bitfield width, where 0 means
the member is not a bitfield:
- The struct and union member parsers store the parsed bit width on the
member instead of discarding it.
- The pretty printer emits " : <width>" before the member's trailing ';',
so "tc"/"tcd" round-trip "struct qwe { int a : 4; int b : 16; int c : 3; }".
- rz_base_type_as_format() (used by "ts"/"tp") now emits the pf packed-bits
spec ":N" for a bitfield member, with the bit order matching the target
endianness ("<" little-endian / ">" big-endian), instead of the member
type's full format. "ts qwe" becomes 'pf ":4<:16<:3< a b c"' and "tp"
unpacks the fields correctly (a=5, b=0x1234, c=0 for 0x00012345 on x86).
Serialization keeps the width in the 3rd comma-field of the existing
"struct.<name>.<member>" / "union.<name>.<member>" value, i.e.
"type,offset,bitsize". That field was already written (always as 0) and
ignored on load, so the on-disk layout is unchanged and only its meaning is
now honored. The project version is bumped to 24 with an additive no-op
migration: an absent or 0 bitsize deserializes as a regular, non-bitfield
member.
Tests: a unit test parses a bitfield struct and union and checks the member
widths, the rendered string and the "pf" format; a db/cmd/types test covers
the full "td"/"tcd"/"ts"/"tp" flow; a project-migration test with a new
v23-bitfield.rzdb fixture covers v23->v24, and the "migrate info" db test is
updated for the new version.
The PDB type parser (librz/arch/pdb_process.c) still drops bitfield members
because their LF_BITFIELD member type resolves to NULL; wiring LF_BITFIELD up
to the new member "size" field, with tests in test/unit/test_pdb.c, is left
as a follow-up.
Co-authored-by: Anton Kochkov <anton.kochkov@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 pf format-string codegen from RzType, the inverse
pf -> C-declaration converter, 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.
Defining types from pf formats (pf → C)
format.c also hosts the reverse helper
rz_type_format_to_c_declaration(name, fmt_str, &error): it parses a
pf format with rz_pf_parse() and emits an equivalent C struct/union
declaration built from the standard fixed-width types (uint8_t, int32_t,
float, ...). Feeding that declaration to the C type parser (as the tdf
command does) registers the format as a first-class RzBaseType, so it then
participates in type analysis -- which a bare pf format never does.
The conversion is purely structural: it consumes only the parsed
RzPfFormat (field kinds, widths, array counts, pointer flags) and never a
byte buffer, so it can run without any target data. A few specifiers have no
exact static C form and are mapped best-effort: @N alignment carries no
storage and is dropped; an unknown-length inline string z becomes
char * (a fixed [N]z becomes char[N]); LEB128 widens to its largest
decoded integer; and ?(Name) / E(Name) emit struct Name / enum Name,
which must themselves be defined types for the declaration to parse.
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.