For some reason claude refuses to review https://github.com/systemd/systemd/pull/42651. Let's see if we can make it work by splitting up the big PR into smaller ones. So here's the first one with 5 commits that should be relatively easy to review and get merged early.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
systemd_sysupdate_sources = files(
|
||||
'sysupdate-cache.c',
|
||||
'sysupdate-cleanup.c',
|
||||
'sysupdate-config.c',
|
||||
'sysupdate-feature.c',
|
||||
'sysupdate-instance.c',
|
||||
'sysupdate-partition.c',
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "log.h"
|
||||
#include "specifier.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "sysupdate-config.h"
|
||||
#include "web-util.h"
|
||||
|
||||
int config_parse_url_specifiers(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
|
||||
const char *root = userdata;
|
||||
char **s = ASSERT_PTR(data);
|
||||
int r;
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = mfree(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(resolved)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return free_and_replace(*s, resolved);
|
||||
}
|
||||
|
||||
int config_parse_url_specifiers_many(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
|
||||
const char *root = userdata;
|
||||
char ***s = ASSERT_PTR(data);
|
||||
int r;
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = strv_free(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(resolved)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strv_consume(s, TAKE_PTR(resolved)) < 0)
|
||||
return log_oom();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "conf-parser-forward.h"
|
||||
|
||||
CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers);
|
||||
CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers_many);
|
||||
@@ -5,9 +5,8 @@
|
||||
#include "hash-funcs.h"
|
||||
#include "path-util.h"
|
||||
#include "string-util.h"
|
||||
#include "sysupdate.h"
|
||||
#include "sysupdate-config.h"
|
||||
#include "sysupdate-feature.h"
|
||||
#include "web-util.h"
|
||||
|
||||
static Feature *feature_free(Feature *f) {
|
||||
if (!f)
|
||||
@@ -42,45 +41,6 @@ DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops,
|
||||
char, string_hash_func, string_compare_func,
|
||||
Feature, feature_unref);
|
||||
|
||||
static int config_parse_url_specifiers(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
char **s = ASSERT_PTR(data);
|
||||
const char *root = ASSERT_PTR(userdata);
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
int r;
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = mfree(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(resolved)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return free_and_replace(*s, resolved);
|
||||
}
|
||||
|
||||
int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *dirs) {
|
||||
assert(f);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "sync-util.h"
|
||||
#include "sysupdate.h"
|
||||
#include "sysupdate-cleanup.h"
|
||||
#include "sysupdate-config.h"
|
||||
#include "sysupdate-feature.h"
|
||||
#include "sysupdate-instance.h"
|
||||
#include "sysupdate-pattern.h"
|
||||
@@ -127,7 +128,7 @@ static int config_parse_protect_version(
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved);
|
||||
r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in ProtectVersion=, ignoring: %s", rvalue);
|
||||
@@ -166,7 +167,7 @@ static int config_parse_min_version(
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved);
|
||||
r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in MinVersion=, ignoring: %s", rvalue);
|
||||
@@ -182,7 +183,7 @@ static int config_parse_min_version(
|
||||
return free_and_replace(*version, resolved);
|
||||
}
|
||||
|
||||
static int config_parse_url_specifiers(
|
||||
static int config_parse_transfer_url_specifiers_many(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
@@ -193,36 +194,13 @@ static int config_parse_url_specifiers(
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
char ***s = ASSERT_PTR(data);
|
||||
|
||||
Transfer *t = ASSERT_PTR(userdata);
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
int r;
|
||||
|
||||
assert(rvalue);
|
||||
/* Here we expect userdata to point to our Transfer object, but config_parse_url_specifiers() wants
|
||||
* the root dir there, hence fix this up */
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = strv_free(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(resolved)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = strv_push(s, TAKE_PTR(resolved));
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
return 0;
|
||||
return config_parse_url_specifiers_many(unit, filename, line, section, section_line, lvalue, ltype, rvalue, data, t->context->root);
|
||||
}
|
||||
|
||||
static int config_parse_current_symlink(
|
||||
@@ -244,7 +222,7 @@ static int config_parse_current_symlink(
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, t->context->root, NULL, &resolved);
|
||||
r = specifier_printf(rvalue, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in CurrentSymlink=, ignoring: %s", rvalue);
|
||||
@@ -333,7 +311,7 @@ static int config_parse_resource_pattern(
|
||||
if (r == 0)
|
||||
break;
|
||||
|
||||
r = specifier_printf(word, NAME_MAX, specifier_table, t->context->root, NULL, &resolved);
|
||||
r = specifier_printf(word, NAME_MAX, system_and_tmp_specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in MatchPattern=, ignoring: %s", rvalue);
|
||||
@@ -377,7 +355,7 @@ static int config_parse_resource_path(
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = specifier_printf(rvalue, PATH_MAX-1, specifier_table, t->context->root, NULL, &resolved);
|
||||
r = specifier_printf(rvalue, PATH_MAX-1, system_and_tmp_specifier_table, t->context->root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in Path=, ignoring: %s", rvalue);
|
||||
@@ -510,33 +488,33 @@ int transfer_read_definition(Transfer *t, const char *path, const char **dirs, H
|
||||
assert(t);
|
||||
|
||||
ConfigTableItem table[] = {
|
||||
{ "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version },
|
||||
{ "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions },
|
||||
{ "Transfer", "Verify", config_parse_bool, 0, &t->verify },
|
||||
{ "Transfer", "ChangeLog", config_parse_url_specifiers, 0, &t->changelog },
|
||||
{ "Transfer", "AppStream", config_parse_url_specifiers, 0, &t->appstream },
|
||||
{ "Transfer", "Features", config_parse_strv, 0, &t->features },
|
||||
{ "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features },
|
||||
{ "Source", "Type", config_parse_resource_type, 0, &t->source.type },
|
||||
{ "Source", "Path", config_parse_resource_path, 0, &t->source },
|
||||
{ "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to },
|
||||
{ "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns },
|
||||
{ "Target", "Type", config_parse_resource_type, 0, &t->target.type },
|
||||
{ "Target", "Path", config_parse_resource_path, 0, &t->target },
|
||||
{ "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to },
|
||||
{ "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns },
|
||||
{ "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target },
|
||||
{ "Target", "PartitionUUID", config_parse_partition_uuid, 0, t },
|
||||
{ "Target", "PartitionFlags", config_parse_partition_flags, 0, t },
|
||||
{ "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto },
|
||||
{ "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs },
|
||||
{ "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only },
|
||||
{ "Target", "Mode", config_parse_mode, 0, &t->mode },
|
||||
{ "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left },
|
||||
{ "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done },
|
||||
{ "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max },
|
||||
{ "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary },
|
||||
{ "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink },
|
||||
{ "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version },
|
||||
{ "Transfer", "ProtectVersion", config_parse_protect_version, 0, &t->protected_versions },
|
||||
{ "Transfer", "Verify", config_parse_bool, 0, &t->verify },
|
||||
{ "Transfer", "ChangeLog", config_parse_transfer_url_specifiers_many, 0, &t->changelog },
|
||||
{ "Transfer", "AppStream", config_parse_transfer_url_specifiers_many, 0, &t->appstream },
|
||||
{ "Transfer", "Features", config_parse_strv, 0, &t->features },
|
||||
{ "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features },
|
||||
{ "Source", "Type", config_parse_resource_type, 0, &t->source.type },
|
||||
{ "Source", "Path", config_parse_resource_path, 0, &t->source },
|
||||
{ "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to },
|
||||
{ "Source", "MatchPattern", config_parse_resource_pattern, 0, &t->source.patterns },
|
||||
{ "Target", "Type", config_parse_resource_type, 0, &t->target.type },
|
||||
{ "Target", "Path", config_parse_resource_path, 0, &t->target },
|
||||
{ "Target", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->target.path_relative_to },
|
||||
{ "Target", "MatchPattern", config_parse_resource_pattern, 0, &t->target.patterns },
|
||||
{ "Target", "MatchPartitionType", config_parse_resource_ptype, 0, &t->target },
|
||||
{ "Target", "PartitionUUID", config_parse_partition_uuid, 0, t },
|
||||
{ "Target", "PartitionFlags", config_parse_partition_flags, 0, t },
|
||||
{ "Target", "PartitionNoAuto", config_parse_tristate, 0, &t->no_auto },
|
||||
{ "Target", "PartitionGrowFileSystem", config_parse_tristate, 0, &t->growfs },
|
||||
{ "Target", "ReadOnly", config_parse_tristate, 0, &t->read_only },
|
||||
{ "Target", "Mode", config_parse_mode, 0, &t->mode },
|
||||
{ "Target", "TriesLeft", config_parse_uint64, 0, &t->tries_left },
|
||||
{ "Target", "TriesDone", config_parse_uint64, 0, &t->tries_done },
|
||||
{ "Target", "InstancesMax", config_parse_instances_max, 0, &t->instances_max },
|
||||
{ "Target", "RemoveTemporary", config_parse_bool, 0, &t->remove_temporary },
|
||||
{ "Target", "CurrentSymlink", config_parse_current_symlink, 0, &t->current_symlink },
|
||||
{}
|
||||
};
|
||||
|
||||
|
||||
+61
-49
@@ -34,7 +34,6 @@
|
||||
#include "pretty-print.h"
|
||||
#include "runtime-scope.h"
|
||||
#include "sort-util.h"
|
||||
#include "specifier.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "sysupdate.h"
|
||||
@@ -74,19 +73,13 @@ STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
|
||||
|
||||
const Specifier specifier_table[] = {
|
||||
COMMON_SYSTEM_SPECIFIERS,
|
||||
COMMON_TMP_SPECIFIERS,
|
||||
{}
|
||||
};
|
||||
|
||||
#define CONTEXT_NULL \
|
||||
(Context) { \
|
||||
.sync = true, \
|
||||
.instances_max = UINT64_MAX, \
|
||||
.verify = -1, \
|
||||
.cleanup = -1, \
|
||||
.installdb_fd = -EBADF, \
|
||||
#define CONTEXT_NULL \
|
||||
(Context) { \
|
||||
.sync = true, \
|
||||
.instances_max = UINT64_MAX, \
|
||||
.verify = -1, \
|
||||
.cleanup = -1, \
|
||||
.installdb_fd = -EBADF, \
|
||||
.target_identifier.class = _TARGET_CLASS_INVALID, \
|
||||
}
|
||||
|
||||
@@ -186,7 +179,50 @@ static void server_done(Server *s) {
|
||||
|
||||
static DEFINE_POINTER_ARRAY_FREE_FUNC(Transfer*, transfer_free);
|
||||
|
||||
static int read_definitions(
|
||||
static int read_features(
|
||||
Context *c,
|
||||
const char **dirs) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
|
||||
ConfFile **files = NULL;
|
||||
size_t n_files = 0;
|
||||
CLEANUP_ARRAY(files, n_files, conf_file_free_array);
|
||||
|
||||
r = conf_files_list_strv_full(
|
||||
".feature",
|
||||
c->root,
|
||||
CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
|
||||
dirs,
|
||||
&files,
|
||||
&n_files);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
|
||||
|
||||
FOREACH_ARRAY(i, files, n_files) {
|
||||
ConfFile *e = *i;
|
||||
|
||||
_cleanup_(feature_unrefp) Feature *f = feature_new();
|
||||
if (!f)
|
||||
return log_oom();
|
||||
|
||||
r = feature_read_definition(f, c->root, e->result, dirs);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
|
||||
|
||||
TAKE_PTR(f);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_transfers(
|
||||
Context *c,
|
||||
const char **dirs,
|
||||
const char *suffix,
|
||||
@@ -279,42 +315,17 @@ static int context_read_definitions(Context *c, const char* node, ReadDefinition
|
||||
if (!dirs)
|
||||
return log_oom();
|
||||
|
||||
ConfFile **files = NULL;
|
||||
size_t n_files = 0;
|
||||
|
||||
CLEANUP_ARRAY(files, n_files, conf_file_free_array);
|
||||
|
||||
r = conf_files_list_strv_full(".feature", c->root,
|
||||
CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
|
||||
(const char**) dirs, &files, &n_files);
|
||||
r = read_features(c, (const char**) dirs);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
|
||||
return r;
|
||||
|
||||
FOREACH_ARRAY(i, files, n_files) {
|
||||
_cleanup_(feature_unrefp) Feature *f = NULL;
|
||||
ConfFile *e = *i;
|
||||
|
||||
f = feature_new();
|
||||
if (!f)
|
||||
return log_oom();
|
||||
|
||||
r = feature_read_definition(f, c->root, e->result, (const char**) dirs);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
|
||||
TAKE_PTR(f);
|
||||
}
|
||||
|
||||
r = read_definitions(c, (const char**) dirs, ".transfer", node);
|
||||
r = read_transfers(c, (const char**) dirs, ".transfer", node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (c->n_transfers + c->n_disabled_transfers == 0) {
|
||||
/* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
|
||||
r = read_definitions(c, (const char**) dirs, ".conf", node);
|
||||
r = read_transfers(c, (const char**) dirs, ".conf", node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -2155,11 +2166,12 @@ static int context_list_components(Context *context, char ***ret_component_names
|
||||
/* Does the system have at least one transfer file in /etc/sysupdate.d, which can be considered a
|
||||
* TARGET_HOST? See target_get_argument() in sysupdated.c */
|
||||
if (ret_has_default_component)
|
||||
*ret_has_default_component = (!context->definitions &&
|
||||
!context->component &&
|
||||
!context->root &&
|
||||
!context->image &&
|
||||
context->n_transfers > 0);
|
||||
*ret_has_default_component =
|
||||
!context->definitions &&
|
||||
!context->component &&
|
||||
!context->root &&
|
||||
!context->image &&
|
||||
context->n_transfers > 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "specifier.h"
|
||||
#include "sysupdate-forward.h"
|
||||
#include "sysupdate-target.h"
|
||||
|
||||
@@ -46,5 +45,3 @@ typedef struct Context {
|
||||
} Context;
|
||||
|
||||
void context_done(Context *c);
|
||||
|
||||
extern const Specifier specifier_table[];
|
||||
|
||||
Reference in New Issue
Block a user