* 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).
18 KiB
pf -- print format
The pf command in Rizin formats raw bytes through a small domain-specific
language. This document is the reference for the language; the implementation
lives under librz/type/pf/ and the public API is <rz_pf.h>.
Quick examples
pf "x4 magic" # 32-bit hex LE named "magic"
pf "x4d4 magic count" # two fields back-to-back, two names
pf "[8]c text" # repeat: 8-char array named "text"
pf "?(_ELF32_HEADER) hdr" # nested struct from typedb, named "hdr"
pf "z[2] s" # length-prefixed string (2-byte LE prefix)
pf "G uuid" # 16-byte GUID (Microsoft mixed-endian default)
pf "B4(R=1,W=2,X=4) perm" # inline bitfield, three flag bits
pf "V(t=u1,l=u2,d=mytab)" # TLV record dispatched via the mytab table
A pf format string has two halves separated by the first ASCII space: the
spec (left of the space, describes the wire-format) and the names
(right of the space, names each field for display). Within either half,
whitespace is not significant; specs and names use specific delimiters.
Spec grammar
The spec is read left-to-right. Each token contributes one field to the output. The token shape determines the field type.
Sized integers
{x,d,u,o,b,X,D,U,O,B}{1,2,4,8}
x-- hex, lowercase letter => little-endianX-- hex, uppercase letter => big-endiand/D-- signed decimal (LE / BE)u/U-- unsigned decimal (LE / BE)o/O-- octal (LE / BE)b/B-- binary (LE / BE)
The digit suffix selects the byte width: 1, 2, 4, or 8 bytes. Examples:
x1 1-byte hex d4 4-byte signed decimal
x2 2-byte hex LE D4 4-byte signed decimal BE
u8 8-byte unsigned b1 1-byte binary
Floats
f{2,4,8} / F{2,4,8} -- IEEE-754 half / single / double, LE / BE.
Bare f / F is accepted but deprecated; use f4 / F8 explicitly.
Strings
z[(encoding)][prefix]
z-- NUL-terminated string in the default encoding (UTF-8).z(<name>)-- encoding override. The name is parsed byrz_str_enc_string_as_type; supported names are:guess-- heuristic detection (default fallback)ascii/8bit-- single-byte passthrough (use for Latin-1)utf8,mutf8-- UTF-8 / Modified UTF-8utf16le,utf16be,utf32le,utf32be-- UTF-16/32ibm037,ibm290-- IBM code pagesebcdices,ebcdicuk,ebcdicus-- EBCDIC variants (Spain, UK, US)settings-- use the active console string encoding
z[N]-- length-prefixed string.Nis the prefix byte width (1, 2, 4, 8). The prefix is read in the field's current endianness; the body bytes follow immediately.s-- pointer to a NUL-terminated string. The pointer is the field's size (defaults to 8 bytes on 64-bit, 4 bytes on 32-bit); the string body is read via the I/O callback.
Legacy Z (UTF-16 LE zstring) and w (2-byte hex LE) are accepted with a
deprecation warning; use z(utf16le) and x2.
Timestamps
t(format)
format is one of:
| Name | Width | Description |
|---|---|---|
unix32 |
4 B | Unix seconds since 1970 (32-bit) |
unix64 |
8 B | Unix seconds since 1970 (64-bit) |
unixms |
8 B | Unix milliseconds |
unixus |
8 B | Unix microseconds |
unixns |
8 B | Unix nanoseconds |
filetime |
8 B | Windows FILETIME (100-ns ticks, 1601) |
dos |
4 B | DOS date |
hfs |
4 B | HFS+ seconds since 1904-01-01 |
oletime |
8 B | OLE automation date (double, days) |
webkit |
8 B | WebKit microseconds since 1601 |
cocoa |
8 B | Cocoa NSDate (double, seconds, 2001) |
Aliases: ntfs maps to filetime.
Bare t / T and t4 / t8 are accepted with a deprecation warning. The
canonical form is t(format).
Composites
?-- nested struct. The typename comes from?(typename)in the spec or(typename)fieldnamein the names list. The named format must already exist in the typedb (e.g. registered viapfn).E-- enum. Behaves like au4lookup against a typedb enum specified via(enumname)fieldnamein the names list.E1/E2/E4/E8to use a different scalar width.B-- bitfield. Two flavours:- Typed:
B4 (perm) flags--permresolved via the typedb. BareBwithout a width uses 4 bytes by default;B1/B2/B4/B8to select the underlying scalar width. - Inline:
B4(R=1,W=2,X=4)-- flags declared inline in parens. The presence of=inside the parens is the discriminator.
- Typed:
G[(layout)]-- 16-byte GUID. Layouts: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.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 (defaultu1)l=<type>-- length scalar (defaultu2)e=le|be-- default endianness for tag and lengthh=v|l|a-- whetherlencovers (v)alue only, (l)+value, or (a)ll including tag+len (defaultv)d=<table>-- dispatch table name. After reading the tag, look uptlv.<table>.<hex-tag>in the typedb; that entry is itself apfformat string that is applied to the value bytes.
Q-- 128-bit unsigned integer (16 bytes, byte-sequential). Endianness matters only for display; the bytes are emitted in buffer order.r-- raw hexdump. Length comes from the[N]repeat prefix; without a 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-- 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 bypf -e le|be ..., by the format's owne=...directive on container specs likeVorB, or by the embedding API caller). Case of the spec letter is not used to pin the endian, unlikex{1,2,4,8}(LE) andX{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
Nform and no baren-- use a1/2/4/8suffix or pickx{1,2,4,8}/X{1,2,4,8}when you do want endian pinned by case.
Pointers
p reads a pointer-sized scalar and prints its value. The width follows
the context bits unless an explicit suffix is given:
p-- pointer inctx.bits/8bytes (default 8 on 64-bit, 4 on 32-bit).p2-- 16-bit pointer (2 bytes).p4-- 32-bit pointer (4 bytes).p8-- 64-bit pointer (8 bytes).
The displayed value is rendered in the current context endianness.
Pointer dereference
*<type> -- read the field's bytes as a pointer, then follow the pointer
through the I/O callback and decode <type> at the dereferenced address.
The displayed line shows both the pointer ((*0x...)) and the dereferenced
payload:
*z-- pointer to NUL-terminated string. Emits(*0xNN) "string".*d4/*x2/*u8-- pointer to a fixed-width scalar. Emits(*0xNN) <value>with the dereferenced value formatted per the inner spec.*?-- pointer to a typedb-registered struct. Emits the(*0xNN)literal followed by the full struct body, recursively. Recursion is bounded byRzPfCtx::max_depthso cyclic pointer chains terminate cleanly.
The pointer itself is read in the field's effective width (see Pointers
above) and endianness. The s specifier is the simpler string-by-pointer
form: it behaves like *z and renders the same (*0xNN) "string" shape
when the deref produced a body, falling back to a bare "" when the
target is unmapped.
Endianness
Endianness is encoded by the case of the spec letter rather than a standalone directive:
- Lowercase (
x,d,u,o,b,f,t, ...) -- little-endian. - Uppercase (
X,D,U,O,B,F,T, ...) -- big-endian.
There is no standalone endian-switch directive in the spec language; each
specifier carries its own endianness. Context-endian forms (n, N, and
the inner reads for pointer dereference) consult RzPfCtx::big_endian
when no explicit case is given.
Repetition and arrays
[N]<type>-- repeat the next field N times (a literal array).[@name]<type>-- repeat the next field N times where N is the integer value of an earlier field namedname. The earlier field must be in the same struct instance.{N}at the top of the format -- repeat the entire format N times.- A leading
0(e.g.0xx4) marks the format as a union: all fields share offset 0; the resulting display offset is0x0everywhere.
Padding and alignment
-
.-- skip one byte (no name consumed). -
@N-- advance to the next offset that is a multiple of N. (No name consumed.) -
:N[<|>]-- read N bits from the current byte cursor::8>or:8-- MSB-first (default):8<-- LSB-first
Consecutive
:Nfields share a bit-level cursor within the current byte; when a non-bit field follows, the cursor snaps to the next byte boundary. -
v(N)/v(N,lsb)/v(N,msb)-- bitvector: read N individual bits (1..4096) and expose them as N separate 0/1 scalars. Consumes exactlyceil(N/8)bytes from the buffer. Unlike:N, a bitvector does not interact with the packed-bit cursor -- eachv(N)field reads whole bytes and stands alone.Bit order within each byte defaults to MSB-first (bit 7 of byte 0 is bit 0 of the vector). Pass
,lsbfor the Intel-style order.Forensics / RE use cases: page-frame allocation maps, NTFS
$Bitmapclusters, ext4 block/inode bitmaps, ELFDT_FLAGS_1, PE characteristics, ACL bitmasks -- anywhere you want to see a bitmap rather than collapse it to a hex number.> wx abcd > pf v(12) bits 0x00000000 : bits = [ 1 0 1 0 1 0 1 1 | 1 1 0 0 ] (12-bit) > pfj "v(16) bits" [{"name":"bits","type":"bitvec","offset":0,"bit_width":16,"value":"1010101111001101"}]JSON renders the vector as a compact string of
'0'/'1'characters alongside abit_widthsibling. Quiet mode (pfq) emits the bits space-separated with no decoration.
Names grammar
A name token can be:
name-- plain field name.(typename)name-- field name with an attached typedb name (used by?for the struct format and byE/Bfor the enum / bitfield name).
If there are more spec fields than names, the trailing fields get auto-
generated names (field_<n>).
Output modes
The same parsed format can be rendered in several modes (selected by the caller, not by the format string):
- text (default) --
<offset> <name> : [endian] <value>one field per line. Nested struct children are indented. Whenscr.coloris enabled, the output is colorised inline via the active console palette: offsets usepal.offset, names usepal.fname, the endian marker usespal.meta, hex/number literals usepal.num, and typedb labels (enum names, bitflag names) usepal.flag. Users can re-theme this via the standardecomechanism. - json -- JSON array of field objects, with nested struct fields under
"fields"and the struct name under"struct_type". - cstruct -- C
struct { ... }declaration mirroring the field types and their decoded values in comments. When invoked aspfc <name>against a typedb-registered format, the format name is included after thestructkeyword (e.g.struct elf_header { ... }). - quiet -- value-only, one per line, no names or offsets. Used by
pfqandpfv. Raw byte-sequential types (Q,r) emit their bytes as a space-separated hex stream. - dot -- Graphviz
digraphwith each top-level field as a record cell inside a singleshape=recordnode. Used bypfd. - structured data -- a value-centric
RzStructuredDatatree (the generic key/value document model shared withrz_bin, ASN.1, and PKCS#7). Exposed throughrz_pf_render_sd()rather than a string mode, since the result is a tree the caller can serialise to JSON or YAML (rz_structured_data_to_json/_to_yaml) or walk with the generic iterator. The top level is a map keyed by field name (unnamed fields becomefield_<n>); scalars map to typed unsigned/signed/ double/string entries, arrays and bitvectors to arrays, nested structs to sub-maps with a_typekey, and raw/GUID payloads to byte blocks. Timestamps emit a formatted string plus a<name>_rawsibling.
Write mode (pfw)
pfw <name>.<field> <value> writes a new value into the field. The field
path can be a dotted navigation into nested structs, e.g.:
pfw gobelin.Buh.first 42
pfw gobelin.Buh.Boh.Bah.Bah.word 0xadde
The walker descends through children at each .; pointer-deref fields
are followed transparently when the target is mapped. Writes go through
the I/O layer (rz_io_write_at) and emit a confirmation line of the
form <field> : <offset> = <value>.
The legacy convention of prefixing pfw with . (to execute the output
as rizin commands) is no longer needed and is not supported -- the new
pfw writes directly. Likewise, the legacy .pf* "execute the rendered
output as commands" form is gone.
Defining types from formats (tdf)
The cstruct output mode above only prints a C struct; the type is
not added to the type database, so it does not drive type analysis. To turn
a pf format into a real, analysis-backed type, use the tdf command
("type define from format"):
tdf <name> <format>
<format> is either a literal pf format string or the name of a format
already saved with pfn / pf.<name>. The format name is resolved first
(exactly like pf <name>), then falls back to being parsed as a literal
format. The new type is registered through the same C-type parser as td,
so afterwards it behaves like any other t type -- it shows up in ts /
tsc, can be cast with tp, linked to addresses, and used as a member of
further td / tdf definitions.
[0x00000000]> tdf rgba "x1x1x1x1 r g b a"
[0x00000000]> tsc rgba
struct rgba {
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
};
[0x00000000]> pfn pixel x1x1x1x1 r g b a # save a named format ...
[0x00000000]> tdf pixel_t pixel # ... then promote it to a type
A leading 0 in the format makes the result a union instead of a
struct. Specifiers are mapped to the standard fixed-width types
(x4/u4 → uint32_t, d2 → int16_t, c → char, f4 → float,
p → void *, s → char *, G → uint8_t[16], and so on). A few
pf specifiers have no exact static C form and are converted best-effort:
@N alignment is dropped (use . / [N]. to materialise padding bytes),
an unknown-length inline z string becomes char * (a fixed [N]z
becomes char[N]), LEB128 widens to its largest decoded integer, and
?(Name) / E(Name) references emit struct Name / enum Name -- which
must themselves already be defined for the new type to register.
Deprecated single-letter aliases
For backward compatibility, the parser still accepts a handful of bare specifiers that map to the new canonical forms, emitting a one-time deprecation diagnostic:
| Legacy | Canonical | Meaning |
|---|---|---|
b |
b1 |
1-byte binary |
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 |
i |
d4 |
4-byte signed decimal LE |
f |
f4 |
IEEE-754 single LE |
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 LE |
T |
T(unix32) |
Unix-32 timestamp BE |
X |
r |
raw hex byte dump |
C |
u1 |
1-byte unsigned decimal |
The single-byte r (raw hexdump) reads one byte; combine it with a
repeat prefix ([16]r) for longer dumps. Note that the legacy
r (regname) register-fetch form is not implemented in the new
parser; restoring it would require a register-lookup hook in the parser
context.
Diagnostics
Parse errors are collected on RzPfFormat::errors[]. Each error has a
severity (WARN / ERROR), a category (SYNTAX, SEMANTIC, RANGE,
DEPRECATED, DATA, DEPTH), and a 0-based column position into the
source format string. rz_pf_format_errors_to_string() renders them with
caret-position lines pointing at the offending column.
The legacy RZ_LOG_WARN channel is preserved for backward compatibility:
every diagnostic is also emitted there. New code that wants structured
errors should walk the errors[] array.