Files
systemd/src/basic/env-file.c
T
Daan De Meyer 74d392ed1b tree-wide: standardize header names across src/fundamental, src/basic and src/shared
Drop the -fundamental suffix from src/fundamental/ headers in favor of names
that match their src/basic/ or src/shared/ counterparts (e.g.
macro-fundamental.h -> macro.h, assert-fundamental.h -> assert-util.h,
cleanup-fundamental.h -> cleanup-util.h). Rename src/basic/{btrfs,label}.{c,h}
to use the -util suffix to match the existing shared/btrfs-util and
shared/label-util siblings. Rename src/shared/mkdir-label.{c,h} to mkdir.{c,h}
and src/shared/tmpfile-util-label.{c,h} to tmpfile-util.{c,h} to match the
corresponding src/basic names.

This saves us from having to come up with separate names for files that do
the same thing across tiers, and it makes it easier to move stuff between
src/fundamental, src/basic and src/shared: consumers just #include "foo.h"
and pick up whichever tier their -I path resolves to first, so call sites
don't need to be updated when an API moves between layers.

Where a higher-tier wrapper exists (e.g. src/basic/macro.h wrapping
src/fundamental/macro.h), the wrapper uses an explicit "../fundamental/foo.h"
or "../basic/foo.h" relative include for the lower-tier header. We can't use
GCC's #include_next directive for this — when the wrapper is reachable both
via same-dir-as-source lookup and via -I (e.g. -Isrc/shared) for the
directory it lives in, #include_next advances by exactly one slot in libcpp's
internal directory chain and lands on the same physical directory it was
already in, never reaching the lower-tier sibling (see make_cpp_dir() in
gcc/libcpp/files.cc:1986).

To make sure the right headers are always picked up, the include directories
are reordered so that e.g. src/shared always takes priority over src/basic and
similar for the other directories.

Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:33:03 +09:00

722 lines
21 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/stat.h>
#include <unistd.h>
#include "alloc-util.h"
#include "env-file.h"
#include "env-util.h"
#include "escape.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
#include "label-util.h"
#include "log.h"
#include "string-util.h"
#include "strv.h"
#include "tmpfile-util.h"
#include "utf8.h"
typedef int (*push_env_func_t)(
const char *filename,
unsigned line,
const char *key,
char *value,
void *userdata);
static int parse_env_file_internal(
FILE *f,
const char *fname,
push_env_func_t push,
void *userdata) {
size_t n_key = 0, n_value = 0, last_value_whitespace = SIZE_MAX, last_key_whitespace = SIZE_MAX;
_cleanup_free_ char *contents = NULL, *key = NULL, *value = NULL;
unsigned line = 1;
int r;
enum {
PRE_KEY,
KEY,
PRE_VALUE,
VALUE,
VALUE_ESCAPE,
SINGLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE_ESCAPE,
COMMENT,
COMMENT_ESCAPE
} state = PRE_KEY;
assert(f || fname);
assert(push);
if (f)
r = read_full_stream(f, &contents, NULL);
else
r = read_full_file(fname, &contents, NULL);
if (r < 0)
return r;
for (char *p = contents; *p; p++) {
char c = *p;
switch (state) {
case PRE_KEY:
if (strchr(COMMENTS, c))
state = COMMENT;
else if (!strchr(WHITESPACE, c)) {
state = KEY;
last_key_whitespace = SIZE_MAX;
if (!GREEDY_REALLOC(key, n_key+2))
return -ENOMEM;
key[n_key++] = c;
}
break;
case KEY:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
n_key = 0;
} else if (c == '=') {
state = PRE_VALUE;
last_value_whitespace = SIZE_MAX;
} else {
if (!strchr(WHITESPACE, c))
last_key_whitespace = SIZE_MAX;
else if (last_key_whitespace == SIZE_MAX)
last_key_whitespace = n_key;
if (!GREEDY_REALLOC(key, n_key+2))
return -ENOMEM;
key[n_key++] = c;
}
break;
case PRE_VALUE:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
key[n_key] = 0;
if (value)
value[n_value] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != SIZE_MAX)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata);
if (r < 0)
return r;
n_key = 0;
value = NULL;
n_value = 0;
} else if (c == '\'')
state = SINGLE_QUOTE_VALUE;
else if (c == '"')
state = DOUBLE_QUOTE_VALUE;
else if (c == '\\')
state = VALUE_ESCAPE;
else if (!strchr(WHITESPACE, c)) {
state = VALUE;
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case VALUE:
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
key[n_key] = 0;
if (value)
value[n_value] = 0;
/* Chomp off trailing whitespace from value */
if (last_value_whitespace != SIZE_MAX)
value[last_value_whitespace] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != SIZE_MAX)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata);
if (r < 0)
return r;
n_key = 0;
value = NULL;
n_value = 0;
} else if (c == '\\') {
state = VALUE_ESCAPE;
last_value_whitespace = SIZE_MAX;
} else {
if (!strchr(WHITESPACE, c))
last_value_whitespace = SIZE_MAX;
else if (last_value_whitespace == SIZE_MAX)
last_value_whitespace = n_value;
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case VALUE_ESCAPE:
state = VALUE;
if (!strchr(NEWLINE, c)) {
/* Escaped newlines we eat up entirely */
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case SINGLE_QUOTE_VALUE:
if (c == '\'')
state = PRE_VALUE;
else {
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case DOUBLE_QUOTE_VALUE:
if (c == '"')
state = PRE_VALUE;
else if (c == '\\')
state = DOUBLE_QUOTE_VALUE_ESCAPE;
else {
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
}
break;
case DOUBLE_QUOTE_VALUE_ESCAPE:
state = DOUBLE_QUOTE_VALUE;
if (strchr(SHELL_NEED_ESCAPE, c)) {
/* If this is a char that needs escaping, just unescape it. */
if (!GREEDY_REALLOC(value, n_value+2))
return -ENOMEM;
value[n_value++] = c;
} else if (c != '\n') {
/* If other char than what needs escaping, keep the "\" in place, like the
* real shell does. */
if (!GREEDY_REALLOC(value, n_value+3))
return -ENOMEM;
value[n_value++] = '\\';
value[n_value++] = c;
}
/* Escaped newlines (aka "continuation lines") are eaten up entirely */
break;
case COMMENT:
if (c == '\\')
state = COMMENT_ESCAPE;
else if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
}
break;
case COMMENT_ESCAPE:
log_debug("The line which doesn't begin with \";\" or \"#\", but follows a comment" \
" line trailing with escape is now treated as a non comment line since v254.");
if (strchr(NEWLINE, c)) {
state = PRE_KEY;
line++;
} else
state = COMMENT;
break;
}
}
if (IN_SET(state,
PRE_VALUE,
VALUE,
VALUE_ESCAPE,
SINGLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE,
DOUBLE_QUOTE_VALUE_ESCAPE)) {
key[n_key] = 0;
if (value)
value[n_value] = 0;
if (state == VALUE)
if (last_value_whitespace != SIZE_MAX)
value[last_value_whitespace] = 0;
/* strip trailing whitespace from key */
if (last_key_whitespace != SIZE_MAX)
key[last_key_whitespace] = 0;
r = push(fname, line, key, value, userdata);
if (r < 0)
return r;
value = NULL;
}
return 0;
}
static int check_utf8ness_and_warn(
const char *filename, unsigned line,
const char *key, char *value) {
assert(key);
if (!utf8_is_valid(key)) {
_cleanup_free_ char *p = NULL;
p = utf8_escape_invalid(key);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s:%u: invalid UTF-8 in key '%s', ignoring.",
strna(filename), line, p);
}
if (value && !utf8_is_valid(value)) {
_cleanup_free_ char *p = NULL;
p = utf8_escape_invalid(value);
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.",
strna(filename), line, key, p);
}
return 0;
}
static int parse_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata) {
const char *k;
va_list aq, *ap = userdata;
int r;
assert(key);
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
va_copy(aq, *ap);
while ((k = va_arg(aq, const char *))) {
char **v;
v = va_arg(aq, char **);
if (streq(key, k)) {
va_end(aq);
free_and_replace(*v, value);
return 1;
}
}
va_end(aq);
free(value);
return 0;
}
int parse_env_filev(
FILE *f,
const char *fname,
va_list ap) {
int r;
va_list aq;
assert(f || fname);
va_copy(aq, ap);
r = parse_env_file_internal(f, fname, parse_env_file_push, &aq);
va_end(aq);
return r;
}
int parse_env_file_fdv(int fd, const char *fname, va_list ap) {
_cleanup_fclose_ FILE *f = NULL;
va_list aq;
int r;
assert(fd >= 0);
r = fdopen_independent(fd, "re", &f);
if (r < 0)
return r;
va_copy(aq, ap);
r = parse_env_file_internal(f, fname, parse_env_file_push, &aq);
va_end(aq);
return r;
}
int parse_env_file_sentinel(
FILE *f,
const char *fname,
...) {
va_list ap;
int r;
assert(f || fname);
va_start(ap, fname);
r = parse_env_filev(f, fname, ap);
va_end(ap);
return r;
}
int parse_env_file_fd_sentinel(
int fd,
const char *fname, /* only used for logging */
...) {
va_list ap;
int r;
assert(fd >= 0);
va_start(ap, fname);
r = parse_env_file_fdv(fd, fname, ap);
va_end(ap);
return r;
}
int parse_env_datav(
const char *data,
size_t size,
const char *fname, /* only used for logging */
va_list ap) {
assert(data);
if (size == SIZE_MAX)
size = strlen_ptr(data);
_cleanup_fclose_ FILE *f = fmemopen_unlocked((void*) data, size, "r");
if (!f)
return -ENOMEM;
return parse_env_filev(f, fname, ap);
}
int parse_env_data_sentinel(
const char *data,
size_t size,
const char *fname, /* only used for logging */
...) {
va_list ap;
int r;
va_start(ap, fname);
r = parse_env_datav(data, size, fname, ap);
va_end(ap);
return r;
}
static int load_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata) {
char ***m = userdata;
char *p;
int r;
assert(key);
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
p = strjoin(key, "=", value);
if (!p)
return -ENOMEM;
r = strv_env_replace_consume(m, p);
if (r < 0)
return r;
free(value);
return 0;
}
int load_env_file(FILE *f, const char *fname, char ***ret) {
_cleanup_strv_free_ char **m = NULL;
int r;
assert(f || fname);
assert(ret);
r = parse_env_file_internal(f, fname, load_env_file_push, &m);
if (r < 0)
return r;
*ret = TAKE_PTR(m);
return 0;
}
static int load_env_file_push_pairs(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata) {
char ***m = ASSERT_PTR(userdata);
int r;
assert(key);
r = check_utf8ness_and_warn(filename, line, key, value);
if (r < 0)
return r;
/* Check if the key is present */
for (char **t = *m; t && *t; t += 2)
if (streq(t[0], key)) {
if (value)
return free_and_replace(t[1], value);
else
return free_and_strdup(t+1, "");
}
r = strv_extend(m, key);
if (r < 0)
return r;
if (value)
return strv_push(m, value);
else
return strv_extend(m, "");
}
int load_env_file_pairs(FILE *f, const char *fname, char ***ret) {
_cleanup_strv_free_ char **m = NULL;
int r;
assert(f || fname);
assert(ret);
r = parse_env_file_internal(f, fname, load_env_file_push_pairs, &m);
if (r < 0)
return r;
*ret = TAKE_PTR(m);
return 0;
}
int load_env_file_pairs_fd(int fd, const char *fname, char ***ret) {
_cleanup_fclose_ FILE *f = NULL;
int r;
assert(fd >= 0);
r = fdopen_independent(fd, "re", &f);
if (r < 0)
return r;
return load_env_file_pairs(f, fname, ret);
}
static int merge_env_file_push(
const char *filename, unsigned line,
const char *key, char *value,
void *userdata) {
char ***env = ASSERT_PTR(userdata);
char *expanded_value;
int r;
assert(key);
if (!value) {
log_error("%s:%u: invalid syntax (around \"%s\"), ignoring.", strna(filename), line, key);
return 0;
}
if (!env_name_is_valid(key)) {
log_error("%s:%u: invalid variable name \"%s\", ignoring.", strna(filename), line, key);
free(value);
return 0;
}
r = replace_env(value,
*env,
REPLACE_ENV_USE_ENVIRONMENT|REPLACE_ENV_ALLOW_BRACELESS|REPLACE_ENV_ALLOW_EXTENDED,
&expanded_value);
if (r < 0)
return log_error_errno(r, "%s:%u: Failed to expand variable '%s': %m", strna(filename), line, value);
free_and_replace(value, expanded_value);
log_debug("%s:%u: setting %s=%s", filename, line, key, value);
return load_env_file_push(filename, line, key, value, env);
}
int merge_env_file(
char ***env,
FILE *f,
const char *fname) {
assert(env);
assert(f || fname);
/* NOTE: this function supports braceful and braceless variable expansions,
* plus "extended" substitutions, unlike other exported parsing functions.
*/
return parse_env_file_internal(f, fname, merge_env_file_push, env);
}
static void env_file_fputs_escaped(FILE *f, const char *p) {
assert(f);
assert(p);
flockfile(f);
if (string_has_cc(p, NULL) || chars_intersect(p, WHITESPACE SHELL_NEED_QUOTES)) {
fputc_unlocked('"', f);
for (; *p; p++) {
if (strchr(SHELL_NEED_ESCAPE, *p))
fputc_unlocked('\\', f);
fputc_unlocked(*p, f);
}
fputc_unlocked('"', f);
} else
fputs_unlocked(p, f);
funlockfile(f);
}
void env_file_fputs_assignment(FILE *f, const char *k, const char *v) {
assert(f);
assert(k);
if (!v)
return;
fputs(k, f);
env_file_fputs_escaped(f, v);
fputc('\n', f);
}
static void write_env_var(FILE *f, const char *v) {
const char *p;
assert(f);
assert(v);
p = strchr(v, '=');
if (!p) {
/* Fallback */
fputs_unlocked(v, f);
fputc_unlocked('\n', f);
return;
}
p++;
fwrite_unlocked(v, 1, p-v, f);
env_file_fputs_escaped(f, p);
fputc_unlocked('\n', f);
}
int write_env_file(int dir_fd, const char *fname, char **headers, char **l, WriteEnvFileFlags flags) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *p = NULL;
int r;
assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
assert(fname);
bool call_label_ops_post = false;
if (FLAGS_SET(flags, WRITE_ENV_FILE_LABEL)) {
r = label_ops_pre(dir_fd, fname, S_IFREG);
if (r < 0)
return r;
call_label_ops_post = true;
}
r = fopen_tmpfile_linkable_at(dir_fd, fname, O_WRONLY|O_CLOEXEC, &p, &f);
int k = call_label_ops_post ? label_ops_post(f ? fileno(f) : dir_fd, f ? NULL : fname, /* created= */ !!f) : 0;
if (r < 0)
return r;
CLEANUP_TMPFILE_AT(dir_fd, p);
if (k < 0)
return k;
r = fchmod_umask(fileno(f), 0644);
if (r < 0)
return r;
STRV_FOREACH(i, headers) {
assert(isempty(*i) || startswith(*i, "#"));
fputs_unlocked(*i, f);
fputc_unlocked('\n', f);
}
STRV_FOREACH(i, l)
write_env_var(f, *i);
r = flink_tmpfile_at(f, dir_fd, p, fname, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC);
if (r < 0)
return r;
p = mfree(p); /* disarm CLEANUP_TMPFILE_AT() */
return 0;
}
int write_vconsole_conf(int dir_fd, const char *fname, char **l) {
char **headers = STRV_MAKE(
"# Written by systemd-localed(8) or systemd-firstboot(1), read by systemd-localed",
"# and systemd-vconsole-setup(8). Use localectl(1) to update this file.");
return write_env_file(dir_fd, fname, headers, l, WRITE_ENV_FILE_LABEL);
}