librz/type: parse, render and size C23 enum underlying types (#3498) (#6433)

C23 lets an enum fix its underlying type, e.g. "enum E : long long { ... }".
The C grammar already exposes it as the "underlying_type" field of an
enum_specifier, and RzBaseType already has a "type" slot documented as
used by enums, but the parser ignored the field and always left it NULL.

- parse_enum_node() now reads the "underlying_type" field and stores the
  parsed type on RzBaseType::type (reusing parse_type_node_single(), so
  primitive, sized and typedef'd integer types are all handled). Classic
  enums keep a NULL underlying type.
- The pretty printer emits " : <type>" between the enum name and its body
  when an underlying type is present, so "tc"/"tcd"/"tec" round-trip it.
- enum_bitsize() now derives the width from the underlying type instead of
  the hardcoded 32-bit default (resolving the long-standing FIXME); it
  still falls back to 32 for a classic enum.

Single-token underlying types (int, uint64_t, char, ...) work end-to-end
with the bundled grammar revision: the existing "enhanced enum" db test is
updated to round-trip "enum v : int" and a unit test covers
"enum EU : uint64_t". Multi-word underlying types (long long, unsigned int)
are added as BROKEN db tests; the bundled grammar parses them to an ERROR
node and drops the underlying type, so these tests fail for now and will
pass once rizin-grammar-c accepts sized type specifiers as the enum
underlying type. No further rizin change is needed for that step.

Co-authored-by: Anton Kochkov <anton.kochkov@gmail.com>
This commit is contained in:
NOT XVilka 2026-05-31 04:01:02 +08:00 committed by GitHub
parent 308d53c9f7
commit e23ac87bc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 347 additions and 33 deletions

View file

@ -754,6 +754,21 @@ RZ_API bool rz_project_migrate_v21_v22(RzProject *prj, RzSerializeResultInfo *re
return true;
}
// --
// Migration 22 -> 23
//
// Changes:
// Enums can now declare a fixed underlying type ("enum E : long long { ... }"),
// serialized under "enum.<name>.@type" in "/core/analysis/types". Older
// projects simply lack that key and the deserializer treats its absence as a
// classic (int-backed) enum, so there is nothing to migrate.
RZ_API bool rz_project_migrate_v22_v23(RzProject *prj, RzSerializeResultInfo *res) {
// there is nothing to be done since the deserializer treats a missing
// "enum.<name>.@type" as a classic enum without a fixed underlying type
return true;
}
static bool (*const migrations[])(RzProject *prj, RzSerializeResultInfo *res) = {
rz_project_migrate_v1_v2,
rz_project_migrate_v2_v3,
@ -776,6 +791,7 @@ static bool (*const migrations[])(RzProject *prj, RzSerializeResultInfo *res) =
rz_project_migrate_v19_v20,
rz_project_migrate_v20_v21,
rz_project_migrate_v21_v22,
rz_project_migrate_v22_v23,
};
/// Migrate the given project to the current version in-place

View file

@ -12,7 +12,7 @@
extern "C" {
#endif
#define RZ_PROJECT_VERSION 22
#define RZ_PROJECT_VERSION 23
typedef Sdb RzProject;
@ -67,6 +67,7 @@ RZ_API bool rz_project_migrate_v18_v19(RzProject *prj, RzSerializeResultInfo *re
RZ_API bool rz_project_migrate_v19_v20(RzProject *prj, RzSerializeResultInfo *res);
RZ_API bool rz_project_migrate_v20_v21(RzProject *prj, RzSerializeResultInfo *res);
RZ_API bool rz_project_migrate_v21_v22(RzProject *prj, RzSerializeResultInfo *res);
RZ_API bool rz_project_migrate_v22_v23(RzProject *prj, RzSerializeResultInfo *res);
RZ_API bool rz_project_migrate(RzProject *prj, unsigned long version, RzSerializeResultInfo *res);
#ifdef __cplusplus

View file

@ -1090,6 +1090,18 @@ int parse_enum_node(CParserState *state, TSNode node, const char *text, ParserTy
}
}
}
// C23 fixed underlying type, e.g. "enum E : unsigned int { ... }". The
// grammar exposes it as the "underlying_type" field; store it on the
// base type (RzBaseType::type), leaving it NULL for a classic enum.
TSNode enum_underlying = ts_node_child_by_field_name(node, "underlying_type", 15);
if (!ts_node_is_null(enum_underlying)) {
ParserTypePair *underlying = NULL;
if (!parse_type_node_single(state, enum_underlying, text, &underlying, false) && underlying) {
rz_type_free(enum_pair->btype->type);
enum_pair->btype->type = underlying->type;
free(underlying);
}
}
// If parsing successfull completed - we store the state
if (enum_pair) {
c_parser_base_type_store(state, name, enum_pair);

View file

@ -33,8 +33,8 @@ static char *get_type_data(Sdb *sdb, const char *type, const char *sname) {
return members;
}
static TypeFormatPair *get_enum_type(Sdb *sdb, const char *sname) {
rz_return_val_if_fail(sdb && RZ_STR_ISNOTEMPTY(sname), NULL);
static TypeFormatPair *get_enum_type(RzTypeDB *typedb, Sdb *sdb, const char *sname) {
rz_return_val_if_fail(typedb && sdb && RZ_STR_ISNOTEMPTY(sname), NULL);
RzBaseType *base_type = rz_type_base_type_new(RZ_BASE_TYPE_KIND_ENUM);
if (!base_type) {
@ -76,6 +76,25 @@ static TypeFormatPair *get_enum_type(Sdb *sdb, const char *sname) {
}
free(members);
// C23 fixed underlying type, stored by save_enum() under "enum.<name>.@type".
// The referenced type may not have been deserialized yet -- base types are read
// from the sdb in an unspecified (hash) order -- but rz_type_parse_string_single()
// turns an unknown name into a forward-looking identifier instead of failing, and
// identifiers are resolved lazily by name when used (e.g. for the enum width), so
// the load order of the underlying type relative to this enum does not matter.
RzStrBuf utkey;
char *underlying = sdb_get(sdb, rz_strbuf_initf(&utkey, "enum.%s.@type", sname));
rz_strbuf_fini(&utkey);
if (underlying) {
char *error_msg = NULL;
RzType *ut = rz_type_parse_string_single(typedb->parser, underlying, &error_msg);
free(underlying);
free(error_msg);
if (ut) {
base_type->type = ut;
}
}
RzStrBuf key;
char *format = sdb_get(sdb, rz_strbuf_initf(&key, "type.%s", sname));
rz_strbuf_fini(&key);
@ -327,7 +346,7 @@ bool sdb_load_base_types(RzTypeDB *typedb, Sdb *sdb) {
if (!strcmp(sdbkv_value(kv), "struct")) {
tpair = get_struct_type(typedb, sdb, sdbkv_key(kv));
} else if (!strcmp(sdbkv_value(kv), "enum")) {
tpair = get_enum_type(sdb, sdbkv_key(kv));
tpair = get_enum_type(typedb, sdb, sdbkv_key(kv));
} else if (!strcmp(sdbkv_value(kv), "union")) {
tpair = get_union_type(typedb, sdb, sdbkv_key(kv));
} else if (!strcmp(sdbkv_value(kv), "typedef")) {
@ -497,6 +516,16 @@ static void save_enum(const RzTypeDB *typedb, Sdb *sdb, const RzBaseType *type)
sdb_set(sdb, key, rz_strbuf_get(&arglist));
free(key);
// C23 fixed underlying type, e.g. "enum E : long long { ... }".
// Stored under a key that cannot clash with a case name.
if (type->type) {
char *underlying = rz_type_as_string(typedb, type->type);
if (underlying) {
sdb_set(sdb, rz_strbuf_setf(&param_key, "enum.%s.@type", sname), underlying);
free(underlying);
}
}
rz_strbuf_fini(&arglist);
rz_strbuf_fini(&param_key);
rz_strbuf_fini(&param_val);

View file

@ -675,7 +675,11 @@ static ut64 atomic_bitsize(const RzTypeDB *typedb, RZ_NONNULL RzBaseType *btype)
static ut64 enum_bitsize(const RzTypeDB *typedb, RZ_NONNULL RzBaseType *btype) {
rz_return_val_if_fail(typedb && btype && btype->kind == RZ_BASE_TYPE_KIND_ENUM, 0);
// FIXME: Need a proper way to determine size of enum
// A C23 enum can fix its underlying type ("enum E : long long { ... }");
// otherwise it defaults to the implementation's int width.
if (btype->type) {
return rz_type_db_get_bitsize(typedb, btype->type);
}
return 32;
}
@ -1043,6 +1047,13 @@ static char *type_as_pretty_string(const RzTypeDB *typedb, const RzType *type, c
case RZ_BASE_TYPE_KIND_ENUM:
if (unfold_all || (is_anon && unfold_anon)) {
RzTypeEnumCase *cas;
if (btype->type) { // C23 fixed underlying type
char *underlying = rz_type_as_string(typedb, btype->type);
if (underlying) {
rz_strbuf_appendf(buf, " : %s", underlying);
free(underlying);
}
}
rz_strbuf_append(buf, " {");
if (multiline) {
indent++; // no recursive call, so manually need to update indent

View file

@ -380,6 +380,7 @@ Detailed project load info:
project migrated from version 19 to 20.
project migrated from version 20 to 21.
project migrated from version 21 to 22.
project migrated from version 22 to 23.
EOF
RUN

View file

@ -586,41 +586,55 @@ tec v
te v
te v 0x123
te v 0x321
EOF
EXPECT=<<EOF
enum v : int { t = 0x123, p = 0x321 };
enum v : int {
t = 0x123,
p = 0x321
};
enum v : int {
t = 0x123,
p = 0x321
};
t = 0x123
p = 0x321
t
p
EOF
RUN
NAME=enhanced enum with long long underlying type
BROKEN=1
FILE==
CMDS=<<EOF
td "enum w : long long { t=0x123, p=0x321 };"
tcd w
tc w
tec w
te w
te w 0x123
te w 0x321
EOF
EXPECT=<<EOF
enum v { t = 0x123, p = 0x321 };
enum v {
enum w : long long { t = 0x123, p = 0x321 };
enum w : long long {
t = 0x123,
p = 0x321
};
enum v {
t = 0x123,
p = 0x321
EOF
RUN
NAME=enhanced enum with unsigned int underlying type
BROKEN=1
FILE==
CMDS=<<EOF
td "enum ui : unsigned int { a=1, b=2 };"
tcd ui
tc ui
EOF
EXPECT=<<EOF
enum ui : unsigned int { a = 0x1, b = 0x2 };
enum ui : unsigned int {
a = 0x1,
b = 0x2
};
t = 0x123
p = 0x321
t
p
enum w { t = 0x123, p = 0x321 };
enum w {
t = 0x123,
p = 0x321
};
enum w {
t = 0x123,
p = 0x321
};
t = 0x123
p = 0x321
t
p
EOF
RUN

View file

@ -391,7 +391,7 @@ echo ---
echo ---
!rz-bin -P ${RZ_FILE} | grep all_control_bits | grep -ao 0x3c09f00
echo ---
!rz-bin -P ${RZ_FILE} | grep "__acrt_fenv_abstract_status {"
!rz-bin -P ${RZ_FILE} | grep "enum __acrt_fenv_abstract_status : uint32_t {"
echo ---
!rz-bin -P ${RZ_FILE} | grep "_towlower_internal"
echo ---
@ -404,7 +404,7 @@ nLength
---
0x3c09f00
---
enum __acrt_fenv_abstract_status {
enum __acrt_fenv_abstract_status : uint32_t {
---
0x00066da4 4366 .text _towlower_internal
---

View file

@ -5,6 +5,7 @@
#include <rz_project.h>
#include "../unit/minunit.h"
#include "test_config.h"
#include "rz_config.h"
#include "sdb.h"
@ -691,6 +692,42 @@ static bool test_migrate_v21_v22_gadget_config() {
mu_end;
}
static bool test_migrate_v22_v23_enum() {
RzProject *prj = rz_project_load_file_raw("prj/v22-enum.rzdb");
mu_assert_notnull(prj, "load raw project");
RzSerializeResultInfo *res = rz_serialize_result_info_new();
bool s = rz_project_migrate_v22_v23(prj, res);
mu_assert_true(s, "v22->v23 migrate success");
Sdb *core_db = sdb_ns(prj, "core", false);
mu_assert_notnull(core_db, "core ns");
Sdb *analysis_db = sdb_ns(core_db, "analysis", false);
mu_assert_notnull(analysis_db, "analysis ns");
Sdb *types_db = sdb_ns(analysis_db, "types", false);
mu_assert_notnull(types_db, "types ns");
// The migration is additive: a classic enum keeps its cases and gains no
// "enum.<name>.@type" key (that key is only written for a fixed underlying type).
mu_assert_streq_free(sdb_get(types_db, "Color"), "enum", "classic enum kind preserved");
mu_assert_null(sdb_get(types_db, "enum.Color.@type"), "classic enum has no underlying-type key");
// It must still deserialize into a classic enum (no underlying type).
RzTypeDB *typedb = rz_type_db_new();
mu_assert_notnull(typedb, "type db new");
rz_type_db_init(typedb, TEST_BUILD_TYPES_DIR, "x86", 64, "linux");
mu_assert_true(rz_serialize_types_load(types_db, typedb, NULL), "types deserialize");
RzBaseType *color = rz_type_db_get_base_type(typedb, "Color");
mu_assert_notnull(color, "Color base type present");
mu_assert_eq(color->kind, RZ_BASE_TYPE_KIND_ENUM, "Color is an enum");
mu_assert_null(color->type, "classic enum has no underlying type");
mu_assert_eq(rz_vector_len(&color->enum_data.cases), 3, "three enum cases preserved");
rz_type_db_free(typedb);
rz_serialize_result_info_free(res);
rz_project_free(prj);
mu_end;
}
/// Load project of given version from file into core and check the log for migration success messages
#define BEGIN_LOAD_TEST(core, version, file) \
do { \
@ -1127,6 +1164,7 @@ int all_tests() {
mu_run_test(test_migrate_v18_v19_str_config);
mu_run_test(test_migrate_v20_v21_debase64);
mu_run_test(test_migrate_v21_v22_gadget_config);
mu_run_test(test_migrate_v22_v23_enum);
mu_run_test(test_load_v1_noreturn);
mu_run_test(test_load_v1_noreturn_empty);
mu_run_test(test_load_v1_unknown_type);

91
test/prj/v22-enum.rzdb Normal file
View file

@ -0,0 +1,91 @@
/
type=rizin rz-db project
version=22
/core
blocksize=0x100
offset=0x80483d0
/core/analysis
/core/analysis/blocks
/core/analysis/callables
/core/analysis/cc
/core/analysis/classes
/core/analysis/classes/attrs
/core/analysis/functions
/core/analysis/hints
/core/analysis/imports
/core/analysis/meta
/core/analysis/meta/spaces
name=CS
spacestack=["*"]
/core/analysis/meta/spaces/spaces
bin=s
/core/analysis/noreturn
/core/analysis/pins
/core/analysis/typelinks
/core/analysis/types
Color=enum
enum.Color=RED,GREEN,BLUE
enum.Color.0x0=RED
enum.Color.0x1=GREEN
enum.Color.0x2=BLUE
enum.Color.BLUE=0x2
enum.Color.GREEN=0x1
enum.Color.RED=0x0
/core/analysis/vars
/core/analysis/xrefs
/core/analysis/zigns
/core/analysis/zigns/spaces
name=zs
spacestack=["*"]
/core/analysis/zigns/spaces/spaces
/core/config
/core/file
relative=../bins/elf/crackme0x05
/core/flags
base=0
realnames=0
/core/flags/flags
/core/flags/spaces
name=fs
spacestack=["*"]
/core/flags/spaces/spaces
classes=s
imports=s
relocs=s
sections=s
segments=s
strings=s
symbols=s
symbols.sections=s
/core/flags/tags
/core/flags/zones

View file

@ -266,9 +266,70 @@ bool test_types_load() {
mu_end;
}
bool test_types_load_enum_underlying_forward() {
// Regression test for the PR #6433 review concern: an enum may fix its
// underlying type ("enum E : T"), serialized as "enum.<name>.@type". Base
// types are read from the sdb in an arbitrary (hash) order, so T may not have
// been loaded yet when the enum is deserialized. The underlying type must be
// kept as a lazily-resolved identifier (not dropped) and must resolve once the
// referenced type is available.
RzTypeDB *typedb = rz_type_db_new();
rz_type_db_set_cpu(typedb, "x86");
rz_type_db_set_bits(typedb, 64);
rz_type_db_set_os(typedb, "linux");
const char *types_dir = TEST_BUILD_TYPES_DIR;
rz_type_db_init(typedb, types_dir, "x86", 64, "linux");
Sdb *db = sdb_new0();
// An enum whose underlying type is a user typedef defined in the same sdb.
// Whichever of the two records is deserialized first, the underlying type must
// end up resolved to the typedef's width.
sdb_set(db, "narciso", "enum");
sdb_set(db, "enum.narciso", "GILLIAN,JAMIE");
sdb_set(db, "enum.narciso.GILLIAN", "0x1");
sdb_set(db, "enum.narciso.JAMIE", "0x2");
sdb_set(db, "enum.narciso.0x1", "GILLIAN");
sdb_set(db, "enum.narciso.0x2", "JAMIE");
sdb_set(db, "enum.narciso.@type", "metal_gear_t");
sdb_set(db, "metal_gear_t", "typedef");
sdb_set(db, "typedef.metal_gear_t", "int64_t");
// An enum whose underlying type is absent from the sdb entirely: the strongest
// form of "not loaded yet". The underlying must still be kept as an identifier.
sdb_set(db, "raiden", "enum");
sdb_set(db, "enum.raiden", "SNAKE");
sdb_set(db, "enum.raiden.SNAKE", "0x0");
sdb_set(db, "enum.raiden.0x0", "SNAKE");
sdb_set(db, "enum.raiden.@type", "ghost_t");
mu_assert_true(rz_serialize_types_load(db, typedb, NULL), "types load");
// enum referencing a typedef present in the same sdb: underlying kept and
// resolved, independent of the deserialization order of the two records.
RzBaseType *narciso = rz_type_db_get_base_type(typedb, "narciso");
mu_assert_notnull(narciso, "narciso loaded");
mu_assert_eq(narciso->kind, RZ_BASE_TYPE_KIND_ENUM, "narciso is an enum");
mu_assert_notnull(narciso->type, "narciso keeps its underlying type");
mu_assert_eq(rz_type_db_get_bitsize(typedb, narciso->type), 64, "underlying resolves to 64-bit");
mu_assert_eq(rz_type_db_base_get_bitsize(typedb, narciso), 64, "enum sized from its underlying type");
// enum referencing a type that is not in the sdb at all: the underlying type is
// still kept as an (as-yet-unresolved) identifier, not dropped.
RzBaseType *raiden = rz_type_db_get_base_type(typedb, "raiden");
mu_assert_notnull(raiden, "raiden loaded");
mu_assert_eq(raiden->kind, RZ_BASE_TYPE_KIND_ENUM, "raiden is an enum");
mu_assert_notnull(raiden->type, "raiden keeps its underlying type even when unresolved");
mu_assert_eq(raiden->type->kind, RZ_TYPE_KIND_IDENTIFIER, "underlying kept as identifier");
mu_assert_streq(raiden->type->identifier.name, "ghost_t", "underlying identifier name kept");
sdb_free(db);
rz_type_db_free(typedb);
mu_end;
}
int all_tests() {
mu_run_test(test_types_save);
mu_run_test(test_types_load);
mu_run_test(test_types_load_enum_underlying_forward);
return tests_passed != tests_run;
}

View file

@ -416,6 +416,45 @@ static bool test_enum_types(void) {
mu_end;
}
static bool test_enum_underlying_type(void) {
RzTypeDB *typedb = rz_type_db_new();
mu_assert_notnull(typedb, "Couldn't create new RzTypeDB");
const char *types_dir = TEST_BUILD_TYPES_DIR;
rz_type_db_init(typedb, types_dir, "x86", 64, "linux");
// C23 enum with a fixed underlying type. A single-token primitive type is
// used here since that is what the bundled grammar revision already parses.
char *error_msg = NULL;
RzType *ttype = rz_type_parse_string_single(typedb->parser, "enum EU : uint64_t { A = 1, B = 2 };", &error_msg);
mu_assert_notnull(ttype, "enum with underlying type parses");
mu_assert_null(error_msg, "no parse error");
RzBaseType *base = rz_type_db_get_base_type(typedb, "EU");
mu_assert_notnull(base, "EU base type exists");
mu_assert_eq(RZ_BASE_TYPE_KIND_ENUM, base->kind, "EU is an enum");
mu_assert_notnull(base->type, "underlying type stored on the base type");
mu_assert_streq_free(rz_type_as_string(typedb, base->type), "uint64_t", "underlying type is uint64_t");
mu_assert_streq_free(rz_type_db_base_type_as_string(typedb, base), "enum EU : uint64_t { A = 0x1, B = 0x2 }", "enum renders its underlying type");
// enum_bitsize() now derives the width from the underlying type via
// rz_type_db_base_get_bitsize(); the concrete value is exercised in the
// integration tests since this unit harness does not load atomic widths.
rz_type_free(ttype);
// A classic enum keeps a NULL underlying type and the default int width
error_msg = NULL;
RzType *plain = rz_type_parse_string_single(typedb->parser, "enum EC { X = 7 };", &error_msg);
mu_assert_notnull(plain, "classic enum parses");
RzBaseType *bc = rz_type_db_get_base_type(typedb, "EC");
mu_assert_notnull(bc, "EC base type exists");
mu_assert_null(bc->type, "classic enum has no underlying type");
mu_assert_streq_free(rz_type_db_base_type_as_string(typedb, bc), "enum EC { X = 0x7 }", "classic enum renders without an underlying type");
mu_assert_eq(rz_type_db_base_get_bitsize(typedb, bc), 32, "classic enum defaults to 32 bits");
rz_type_free(plain);
rz_type_db_free(typedb);
mu_end;
}
static bool test_const_types(void) {
RzTypeDB *typedb = rz_type_db_new();
mu_assert_notnull(typedb, "Couldn't create new RzTypeDB");
@ -1969,6 +2008,7 @@ int all_tests() {
mu_run_test(test_type_as_string);
mu_run_test(test_type_as_pretty_string);
mu_run_test(test_enum_types);
mu_run_test(test_enum_underlying_type);
mu_run_test(test_const_types);
mu_run_test(test_array_types);
mu_run_test(test_single_typedef_aliases);