From c2095c11910e4f022a301fd0e63eed390cdc4250 Mon Sep 17 00:00:00 2001 From: Alberto Leiva Popper Date: Tue, 20 May 2025 18:05:14 -0600 Subject: [PATCH] Resolve more simple critical TODOs - Rename `file_exists()` to `file_stat_errno()` - Make result string `node2file()` consistently free-able - Move `valid_file_or_dir()` to file.c, rename to `file_is_valid()` - Make cache threshold configurable - Recover from errors in `__vfprintf()` - Remove recursion from `cer_cleanup()` and `cer_free()` - Delete snapshots and deltas after exploding them --- src/cache.c | 19 ++++++++------ src/cache.h | 2 +- src/common.c | 23 ----------------- src/common.h | 3 --- src/config.c | 33 +++++++++++++++++-------- src/file.c | 26 ++++++++++++++++++-- src/file.h | 3 ++- src/log.c | 29 ++++++++++++++-------- src/object/certificate.c | 49 ++++++++++++++++++++++++------------- src/object/manifest.c | 6 ++--- src/rrdp.c | 4 +-- test/cache_test.c | 22 ++++++++--------- test/file_test.c | 4 +-- test/mock.c | 21 +++++++++++----- test/rtr/db/vrps_test.c | 1 + test/rtr/pdu_handler_test.c | 1 + 16 files changed, 146 insertions(+), 100 deletions(-) diff --git a/src/cache.c b/src/cache.c index 7fa67cb3..b3bcd60d 100644 --- a/src/cache.c +++ b/src/cache.c @@ -95,7 +95,7 @@ struct cache_node { enum node_state state; /* Result code of recent dl attempt (DLS_FRESH only) */ validation_verdict verdict; - time_t attempt_ts; /* Refresh: Dl attempt. Fallback: Unused */ + time_t attempt_ts; /* Refresh: Dl attempt. Fallback: Commit */ time_t success_ts; /* Refresh: Dl success. Fallback: Commit */ struct mft_meta mft; /* RPP fallbacks only */ @@ -357,7 +357,7 @@ static void init_cachedir_tag(void) { static char const *filename = "CACHEDIR.TAG"; - if (file_exists(filename) == ENOENT) + if (file_stat_errno(filename) == ENOENT) file_write_txt(filename, "Signature: 8a477f597d28d172789f06886806bc55\n" "# This file is a cache directory tag created by Fort.\n" @@ -1102,18 +1102,19 @@ refresh_success: return VV_CONTINUE; } -static char const * +/* Result needs free() */ +static char * node2file(struct cache_node const *node, struct uri const *url) { if (node == NULL) return NULL; - // XXX RRDP is const, rsync needs to be freed return (node->rrdp) - ? /* RRDP */ rrdp_file(node->rrdp, url) + ? /* RRDP */ pstrdup(rrdp_file(node->rrdp, url)) : /* rsync */ path_join(node->path, strip_rsync_module(uri_str(url))); } -char const * +/* Result needs free() */ +char * cage_map_file(struct cache_cage *cage, struct uri const *url) { /* @@ -1122,7 +1123,7 @@ cage_map_file(struct cache_cage *cage, struct uri const *url) * modified either. */ - char const *file; + char *file; file = node2file(cage->refresh, url); if (!file) @@ -1400,6 +1401,7 @@ commit_fallbacks(time_t now) fb = provide_node(&cache.fallback, &commit->rpkiNotify, &commit->caRepository); + fb->attempt_ts = now; fb->success_ts = now; pr_op_debug("mkdir -f %s", fb->path); @@ -1424,6 +1426,7 @@ commit_fallbacks(time_t now) uri_str(&map->url)); fb = provide_node(&cache.fallback, &map->url, NULL); + fb->attempt_ts = now; fb->success_ts = now; if (is_fallback(map->path)) goto freshen; @@ -1466,7 +1469,7 @@ static void remove_orphaned_nodes(struct cache_table *table, struct cache_node *node, void *arg) { - if (file_exists(node->path) == ENOENT) { + if (file_stat_errno(node->path) == ENOENT) { pr_op_debug("Missing file; deleting node: %s", node->path); delete_node(table, node, NULL); } diff --git a/src/cache.h b/src/cache.h index 1e30014c..9d04a1da 100644 --- a/src/cache.h +++ b/src/cache.h @@ -44,7 +44,7 @@ validation_verdict cache_get_fallback(struct uri const *, char const **); struct cache_cage; validation_verdict cache_refresh_by_uris(struct extension_uris *, struct cache_cage **); -char const *cage_map_file(struct cache_cage *, struct uri const *); +char *cage_map_file(struct cache_cage *, struct uri const *); bool cage_downgrade(struct cache_cage *); struct mft_meta const *cage_mft_fallback(struct cache_cage *); void cache_commit_rpp(struct uri const *, struct uri const *, struct rpp *); diff --git a/src/common.c b/src/common.c index 297f6c1d..12383c7a 100644 --- a/src/common.c +++ b/src/common.c @@ -220,29 +220,6 @@ foreach_file(char const *location, char const *file_ext, bool empty_err, return process_dir_files(location, file_ext, empty_err, cb, arg); } -bool -valid_file_or_dir(char const *location, bool check_file) -{ - struct stat attr; - bool is_file, is_dir; - bool result; - - if (stat(location, &attr) == -1) { - pr_op_err("stat(%s) failed: %s", location, strerror(errno)); - return false; - } - - is_file = check_file && S_ISREG(attr.st_mode); - is_dir = S_ISDIR(attr.st_mode); - - result = is_file || is_dir; - if (!result) - pr_op_err("'%s' does not seem to be a %s", location, - check_file ? "file or directory" : "directory"); - - return result; -} - time_t time_nonfatal(void) { diff --git a/src/common.h b/src/common.h index 7068281e..09eb8e43 100644 --- a/src/common.h +++ b/src/common.h @@ -40,9 +40,6 @@ void rwlock_unlock(pthread_rwlock_t *); typedef int (*foreach_file_cb)(char const *, void *); int foreach_file(char const *, char const *, bool, foreach_file_cb, void *); -// XXX -bool valid_file_or_dir(char const *, bool); - time_t time_nonfatal(void); time_t time_fatal(void); diff --git a/src/config.c b/src/config.c index de0f8dc4..ea8b14e2 100644 --- a/src/config.c +++ b/src/config.c @@ -10,7 +10,6 @@ #include #include "alloc.h" -#include "common.h" #include "config/boolean.h" #include "config/incidences.h" #include "config/str.h" @@ -19,6 +18,7 @@ #include "config/work_offline.h" #include "configure_ac.h" #include "daemon.h" +#include "file.h" #include "init.h" #include "json_handler.h" #include "log.h" @@ -30,7 +30,7 @@ * To add a member to this structure, * * 1. Add it. - * 2. Add its metadata somewhere in @groups. + * 2. Add its metadata somewhere in @options. * 3. Add default value to set_default_values(). * 4. Create the getter. * @@ -39,8 +39,14 @@ struct rpki_config { /** TAL file name or directory. */ char *tal; - /** Path of our local clone of the repository */ - char *local_repository; + + struct { + /* Path of our local clone of the repository */ + char *path; + /* Cache content expiration seconds (after last refresh) */ + unsigned int threshold; + } cache; + /* Deprecated; does nothing. */ bool shuffle_tal_uris; /** @@ -288,10 +294,16 @@ static const struct option_field options[] = { .id = 'r', .name = "local-repository", .type = >_string, - .offset = offsetof(struct rpki_config, local_repository), + .offset = offsetof(struct rpki_config, cache.path), .doc = "Directory where the repository local cache will be stored/read", .arg_doc = "", .json_null_allowed = false, + }, { + .id = 1001, + .name = "cache.threshold", + .type = >_uint, + .offset = offsetof(struct rpki_config, cache.threshold), + .doc = "Cache content expiration seconds (after last refresh)", }, { .id = 2001, .name = "shuffle-uris", @@ -942,7 +954,8 @@ set_default_values(void) */ rpki_config.tal = NULL; - rpki_config.local_repository = pstrdup("/tmp/fort/repository"); + rpki_config.cache.path = pstrdup("/tmp/fort/repository"); + rpki_config.cache.threshold = 86400; rpki_config.shuffle_tal_uris = false; rpki_config.maximum_certificate_depth = 32; rpki_config.slurm = NULL; @@ -1024,7 +1037,7 @@ validate_config(void) return pr_op_err("The TAL(s) location (--tal) is mandatory."); /* A file location at --tal isn't valid when --init-tals is set */ - if (!valid_file_or_dir(rpki_config.tal, !rpki_config.init_tals)) + if (!file_is_valid(rpki_config.tal, !rpki_config.init_tals)) return pr_op_err("Invalid TAL(s) location."); /* Ignore the other checks */ @@ -1037,7 +1050,7 @@ validate_config(void) rpki_config.server.interval.retry) return pr_op_err("Expire interval must be greater than refresh and retry intervals"); - if (rpki_config.slurm != NULL && !valid_file_or_dir(rpki_config.slurm, true)) + if (rpki_config.slurm != NULL && !file_is_valid(rpki_config.slurm, true)) return pr_op_err("Invalid slurm location."); return 0; @@ -1272,13 +1285,13 @@ config_get_tal(void) char const * config_get_local_repository(void) { - return rpki_config.local_repository; + return rpki_config.cache.path; } time_t cfg_cache_threshold(void) { - return 86400; // XXX + return rpki_config.cache.threshold; } unsigned int diff --git a/src/file.c b/src/file.c index 59775f44..a46a9bd1 100644 --- a/src/file.c +++ b/src/file.c @@ -165,14 +165,36 @@ file_free(struct file_contents *fc) } /* Wrapper for stat(), mostly for the sake of unit test mocking. */ -/* XXX needs a rename, because it returns errno. */ int -file_exists(char const *path) +file_stat_errno(char const *path) { struct stat meta; return (stat(path, &meta) == 0) ? 0 : errno; } +bool +file_is_valid(char const *location, bool allow_file) +{ + struct stat attr; + bool is_file, is_dir; + bool result; + + if (stat(location, &attr) == -1) { + pr_op_err("stat(%s) failed: %s", location, strerror(errno)); + return false; + } + + is_file = allow_file && S_ISREG(attr.st_mode); + is_dir = S_ISDIR(attr.st_mode); + + result = is_file || is_dir; + if (!result) + pr_op_err("'%s' does not seem to be a %s", location, + allow_file ? "file or directory" : "directory"); + + return result; +} + /* * Like remove(), but don't care if the file is already deleted. */ diff --git a/src/file.h b/src/file.h index 013779b2..8041c695 100644 --- a/src/file.h +++ b/src/file.h @@ -32,7 +32,8 @@ void file_close(FILE *); int file_load(char const *, struct file_contents *, bool); void file_free(struct file_contents *); -int file_exists(char const *); +int file_stat_errno(char const *); +bool file_is_valid(char const *, bool); int file_rm_f(char const *); int file_rm_rf(char const *); diff --git a/src/log.c b/src/log.c index 9c3c8679..66b0de1e 100644 --- a/src/log.c +++ b/src/log.c @@ -272,13 +272,28 @@ unlock_mutex(void) print_stack_trace(strerror(error)); /* Same as above. */ } +static void +print_time(struct level const *lvl) +{ + time_t now; + struct tm components; + char str[16]; + + now = time(NULL); + if (now == ((time_t) -1)) + return; + if (localtime_r(&now, &components) == NULL) + return; + if (strftime(str, sizeof(str), "%m-%d %H:%M:%S", &components) == 0) + return; + + fprintf(lvl->stream, "%s ", str); +} + static void __vfprintf(int level, struct log_config *cfg, char const *format, va_list args) { - char time_buff[20]; struct level const *lvl; - time_t now; - struct tm stm_buff; char const *file_name; lvl = level2struct(level); @@ -288,13 +303,7 @@ __vfprintf(int level, struct log_config *cfg, char const *format, va_list args) if (cfg->color) fprintf(lvl->stream, "%s", lvl->color); - now = time(NULL); - if (now != ((time_t) -1)) { - // XXX not catching any errors - localtime_r(&now, &stm_buff); - strftime(time_buff, sizeof(time_buff), "%b %e %T", &stm_buff); - fprintf(lvl->stream, "%s ", time_buff); - } + print_time(lvl); fprintf(lvl->stream, "%s", lvl->label); if (cfg->tag) diff --git a/src/object/certificate.c b/src/object/certificate.c index 80b903f3..fce74a41 100644 --- a/src/object/certificate.c +++ b/src/object/certificate.c @@ -789,27 +789,39 @@ cer_init_ee(struct rpki_certificate *ee, struct rpki_certificate *parent, atomic_fetch_add(&parent->refcount, 1); } +static void +__cer_cleanup(struct rpki_certificate *cer) +{ + map_cleanup(&cer->map); + if (cer->x509 != NULL) + X509_free(cer->x509); + resources_destroy(cer->resources); + exturis_cleanup(&cer->uris); + rpp_cleanup(&cer->rpp); +} + void -cer_cleanup(struct rpki_certificate *cert) +cer_cleanup(struct rpki_certificate *cer) { - map_cleanup(&cert->map); - if (cert->x509 != NULL) - X509_free(cert->x509); - resources_destroy(cert->resources); - exturis_cleanup(&cert->uris); - // XXX Recursive. Try refcounting the resources. - if (cert->parent) - cer_free(cert->parent); - rpp_cleanup(&cert->rpp); + __cer_cleanup(cer); + if (cer->parent) + cer_free(cer->parent); } void -cer_free(struct rpki_certificate *cert) +cer_free(struct rpki_certificate *cer) { - if (atomic_fetch_sub(&cert->refcount, 1) == 1) { - cer_cleanup(cert); - free(cert); - } + struct rpki_certificate *parent; + + do { + if (atomic_fetch_sub(&cer->refcount, 1) != 1) + return; + + __cer_cleanup(cer); + parent = cer->parent; + free(cer); + cer = parent; + } while (cer != NULL); } /* @@ -1941,6 +1953,7 @@ cer_traverse(struct rpki_certificate *ca) struct cache_mapping *map; unsigned int queued; validation_verdict vv; + int error; if (!ca->x509) { if (validate_certificate(ca) != 0) @@ -1961,7 +1974,7 @@ cer_traverse(struct rpki_certificate *ca) } mft.url = ca->uris.rpkiManifest; -retry: mft.path = (char *)cage_map_file(cage, &mft.url); /* Will not edit */ +retry: mft.path = cage_map_file(cage, &mft.url); if (!mft.path) { if (cage_downgrade(cage)) goto retry; @@ -1971,7 +1984,9 @@ retry: mft.path = (char *)cage_map_file(cage, &mft.url); /* Will not edit */ goto end; } - if (manifest_traverse(&mft, cage, ca) != 0) { + error = manifest_traverse(&mft, cage, ca); + free(mft.path); + if (error) { if (cage_downgrade(cage)) goto retry; vv = VV_FAIL; diff --git a/src/object/manifest.c b/src/object/manifest.c index 1eb9bbad..514db9bc 100644 --- a/src/object/manifest.c +++ b/src/object/manifest.c @@ -292,7 +292,6 @@ collect_files(struct cache_mapping const *map, struct FileAndHash *src; struct cache_mapping *dst; char const *ext; - char const *path; int error; if (mft->fileList.list.count == 0) @@ -338,14 +337,13 @@ collect_files(struct cache_mapping const *map, uri_child(&rpp_url, (char const *)src->file.buf, src->file.size, &dst->url); - path = cage_map_file(cage, &dst->url); - if (!path) { + dst->path = cage_map_file(cage, &dst->url); + if (!dst->path) { error = pr_val_err( "Manifest file '%s' is absent from the cache.", uri_str(&dst->url)); goto revert; } - dst->path = pstrdup(path); error = check_file_and_hash(src, dst->path); if (error) diff --git a/src/rrdp.c b/src/rrdp.c index 1016161e..8795364f 100644 --- a/src/rrdp.c +++ b/src/rrdp.c @@ -944,7 +944,7 @@ handle_snapshot(struct update_notification *new, struct rrdp_state *state) if (error) goto end; error = parse_snapshot(&new->session, tmppath, state); -// delete_file(tmppath); XXX + file_rm_f(tmppath); end: fnstack_pop(); return error; @@ -1016,7 +1016,7 @@ handle_delta(struct update_notification *notif, if (error) goto end; error = parse_delta(notif, delta, tmppath, state); -// delete_file(tmppath); XXX + file_rm_f(tmppath); end: fnstack_pop(); return error; diff --git a/test/cache_test.c b/test/cache_test.c index 6908c05a..a094f7b8 100644 --- a/test/cache_test.c +++ b/test/cache_test.c @@ -96,7 +96,7 @@ setup_test(void) init_tables(); for (d = dirs; *d; d++) { - if (file_exists(*d) == 0) + if (file_stat_errno(*d) == 0) ck_assert_int_eq(0, file_rm_rf(*d)); ck_assert_int_eq(0, mkdir(*d, CACHE_FILEMODE)); } @@ -192,9 +192,9 @@ ck_cage_map(struct cache_cage *cage, char const *url, refresh = cage->refresh; fallback = cage->fallback; - ck_assert_pstr_eq(opt1, cage_map_file(cage, &uri)); + ck_assert_pstr_eq_free(opt1, cage_map_file(cage, &uri)); ck_assert_uint_eq(!!opt2, cage_downgrade(cage)); - ck_assert_pstr_eq(opt2, cage_map_file(cage, &uri)); + ck_assert_pstr_eq_free(opt2, cage_map_file(cage, &uri)); cage->refresh = refresh; cage->fallback = fallback; @@ -866,9 +866,9 @@ START_TEST(test_context) ck_assert_ptr_eq(NULL, uri_init(&sias.caRepository, CA_REPOSITORY)); ck_assert_str_eq(VV_CONTINUE, cache_refresh_by_uris(&sias, &cage)); ck_assert_str_eq(RPKI_NOTIFY, uri_str(&cage->rpkiNotify)); - ck_assert_str_eq(FILE_RRDP_PATH, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(FILE_RRDP_PATH, cage_map_file(cage, &file_url)); ck_assert_int_eq(false, cage_downgrade(cage)); - ck_assert_ptr_eq(NULL, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(NULL, cage_map_file(cage, &file_url)); printf("2. 2nd CA points to the same caRepository,\n"); printf(" but does not provide RRDP as an option.\n"); @@ -880,9 +880,9 @@ START_TEST(test_context) ck_assert_str_eq(VV_CONTINUE, cache_refresh_by_uris(&sias, &cage)); ck_assert_ptr_eq(NULL, uri_str(&cage->rpkiNotify)); - ck_assert_str_eq(FILE_RSYNC_PATH, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(FILE_RSYNC_PATH, cage_map_file(cage, &file_url)); ck_assert_int_eq(false, cage_downgrade(cage)); - ck_assert_ptr_eq(NULL, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(NULL, cage_map_file(cage, &file_url)); printf("3. Commit\n"); @@ -907,16 +907,16 @@ START_TEST(test_context) print_tree(); ck_assert_str_eq(VV_CONTINUE, cache_refresh_by_uris(&sias, &cage)); ck_assert_ptr_eq(NULL, uri_str(&cage->rpkiNotify)); - ck_assert_str_eq(FILE_RSYNC_PATH, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(FILE_RSYNC_PATH, cage_map_file(cage, &file_url)); ck_assert_int_eq(true, cage_downgrade(cage)); - ck_assert_str_eq("fallback/1/0", cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free("fallback/1/0", cage_map_file(cage, &file_url)); ck_assert_ptr_eq(NULL, uri_init(&sias.rpkiNotify, RPKI_NOTIFY)); ck_assert_str_eq(VV_CONTINUE, cache_refresh_by_uris(&sias, &cage)); ck_assert_str_eq(RPKI_NOTIFY, uri_str(&cage->rpkiNotify)); - ck_assert_str_eq(FILE_RRDP_PATH, cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free(FILE_RRDP_PATH, cage_map_file(cage, &file_url)); ck_assert_int_eq(true, cage_downgrade(cage)); - ck_assert_str_eq("fallback/0/0", cage_map_file(cage, &file_url)); + ck_assert_pstr_eq_free("fallback/0/0", cage_map_file(cage, &file_url)); uri_cleanup(&sias.rpkiNotify); uri_cleanup(&sias.caRepository); diff --git a/test/file_test.c b/test/file_test.c index a18a1b3b..66031232 100644 --- a/test/file_test.c +++ b/test/file_test.c @@ -24,9 +24,9 @@ START_TEST(test_rm) { create_test_sandbox(); - ck_assert_int_eq(0, file_exists("tmp/file/abc")); + ck_assert_int_eq(0, file_stat_errno("tmp/file/abc")); ck_assert_int_eq(0, file_rm_rf("tmp/file/abc")); - ck_assert_int_eq(ENOENT, file_exists("tmp/file/abc")); + ck_assert_int_eq(ENOENT, file_stat_errno("tmp/file/abc")); } END_TEST diff --git a/test/mock.c b/test/mock.c index 72827402..8aaf4bd2 100644 --- a/test/mock.c +++ b/test/mock.c @@ -2,10 +2,15 @@ #include #include +#include +#include +#include +#include #include -#include "config.h" + +#include "common.h" #include "log.h" -#include "thread_var.h" +#include "types/map.h" /* Some core functions, as linked from unit tests. */ @@ -113,7 +118,6 @@ v6addr2str2(struct in6_addr const *addr) MOCK_NULL(config_get_slurm, char const *, void) MOCK(config_get_tal, char const *, "tal/", void) MOCK(cfg_cache_threshold, time_t, 2, void) -MOCK(config_get_mode, enum mode, STANDALONE, void) MOCK_UINT(config_get_rrdp_delta_threshold, 5, void) MOCK_TRUE(config_get_rsync_enabled, void) MOCK_UINT(config_get_rsync_priority, 50, void) @@ -121,8 +125,6 @@ MOCK_TRUE(config_get_http_enabled, void) MOCK_UINT(config_get_http_priority, 60, void) MOCK_NULL(config_get_output_roa, char const *, void) MOCK_NULL(config_get_output_bgpsec, char const *, void) -MOCK(config_get_op_log_file_format, enum filename_format, FNF_NAME, void) -MOCK(config_get_val_log_file_format, enum filename_format, FNF_NAME, void) MOCK(logv_filename, char const *, path, char const *path) MOCK_VOID(free_rpki_config, void) @@ -132,6 +134,13 @@ MOCK_VOID(fnstack_push_map, struct cache_mapping const *map) MOCK_VOID(fnstack_pop, void) MOCK_VOID(fnstack_cleanup, void) +void +ck_assert_pstr_eq_free(char const *expected, char *actual) +{ + ck_assert_pstr_eq(expected, actual); + free(actual); +} + void ck_assert_uri(char const *expected, struct uri const *actual) { @@ -142,7 +151,7 @@ ck_assert_uri(char const *expected, struct uri const *actual) void touch_dir(char const *dir) { - ck_assert_int_eq(0, file_mkdir(dir, true)); + ck_assert(mkdir(dir, CACHE_FILEMODE) == 0 || errno == EEXIST); } void diff --git a/test/rtr/db/vrps_test.c b/test/rtr/db/vrps_test.c index c529f385..cd969b95 100644 --- a/test/rtr/db/vrps_test.c +++ b/test/rtr/db/vrps_test.c @@ -62,6 +62,7 @@ static const bool deltas_4to4[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; static unsigned int deltas_lifetime = 5; +MOCK(config_get_mode, enum mode, STANDALONE, void) MOCK_UINT(config_get_deltas_lifetime, deltas_lifetime, void) MOCK_ABORT_ENUM(config_get_output_format, output_format, void) MOCK_ABORT_VOID(db_slurm_destroy, struct db_slurm *db) diff --git a/test/rtr/pdu_handler_test.c b/test/rtr/pdu_handler_test.c index df781958..1390e34f 100644 --- a/test/rtr/pdu_handler_test.c +++ b/test/rtr/pdu_handler_test.c @@ -23,6 +23,7 @@ MOCK_INT(slurm_apply, 0, struct db_table *base, struct db_slurm **slurm) MOCK_ABORT_VOID(db_slurm_destroy, struct db_slurm *db) MOCK_VOID(output_print_data, struct db_table const *db) __MOCK_ABORT(config_get_local_repository, char const *, "tmp/pdu", void) +MOCK(config_get_mode, enum mode, STANDALONE, void) /* Mocks end */ -- 2.47.2