]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/basic/env-file.c
vmspawn: Run auxiliary daemons inside scope instead of separate service (#38047)
[thirdparty/systemd.git] / src / basic / env-file.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include <sys/stat.h>
4 #include <unistd.h>
5
6 #include "alloc-util.h"
7 #include "env-file.h"
8 #include "env-util.h"
9 #include "errno-util.h"
10 #include "escape.h"
11 #include "fd-util.h"
12 #include "fileio.h"
13 #include "fs-util.h"
14 #include "label.h"
15 #include "log.h"
16 #include "string-util.h"
17 #include "strv.h"
18 #include "tmpfile-util.h"
19 #include "utf8.h"
20
21 typedef int (*push_env_func_t)(
22 const char *filename,
23 unsigned line,
24 const char *key,
25 char *value,
26 void *userdata);
27
28 static int parse_env_file_internal(
29 FILE *f,
30 const char *fname,
31 push_env_func_t push,
32 void *userdata) {
33
34 size_t n_key = 0, n_value = 0, last_value_whitespace = SIZE_MAX, last_key_whitespace = SIZE_MAX;
35 _cleanup_free_ char *contents = NULL, *key = NULL, *value = NULL;
36 unsigned line = 1;
37 int r;
38
39 enum {
40 PRE_KEY,
41 KEY,
42 PRE_VALUE,
43 VALUE,
44 VALUE_ESCAPE,
45 SINGLE_QUOTE_VALUE,
46 DOUBLE_QUOTE_VALUE,
47 DOUBLE_QUOTE_VALUE_ESCAPE,
48 COMMENT,
49 COMMENT_ESCAPE
50 } state = PRE_KEY;
51
52 assert(f || fname);
53 assert(push);
54
55 if (f)
56 r = read_full_stream(f, &contents, NULL);
57 else
58 r = read_full_file(fname, &contents, NULL);
59 if (r < 0)
60 return r;
61
62 for (char *p = contents; *p; p++) {
63 char c = *p;
64
65 switch (state) {
66
67 case PRE_KEY:
68 if (strchr(COMMENTS, c))
69 state = COMMENT;
70 else if (!strchr(WHITESPACE, c)) {
71 state = KEY;
72 last_key_whitespace = SIZE_MAX;
73
74 if (!GREEDY_REALLOC(key, n_key+2))
75 return -ENOMEM;
76
77 key[n_key++] = c;
78 }
79 break;
80
81 case KEY:
82 if (strchr(NEWLINE, c)) {
83 state = PRE_KEY;
84 line++;
85 n_key = 0;
86 } else if (c == '=') {
87 state = PRE_VALUE;
88 last_value_whitespace = SIZE_MAX;
89 } else {
90 if (!strchr(WHITESPACE, c))
91 last_key_whitespace = SIZE_MAX;
92 else if (last_key_whitespace == SIZE_MAX)
93 last_key_whitespace = n_key;
94
95 if (!GREEDY_REALLOC(key, n_key+2))
96 return -ENOMEM;
97
98 key[n_key++] = c;
99 }
100
101 break;
102
103 case PRE_VALUE:
104 if (strchr(NEWLINE, c)) {
105 state = PRE_KEY;
106 line++;
107 key[n_key] = 0;
108
109 if (value)
110 value[n_value] = 0;
111
112 /* strip trailing whitespace from key */
113 if (last_key_whitespace != SIZE_MAX)
114 key[last_key_whitespace] = 0;
115
116 r = push(fname, line, key, value, userdata);
117 if (r < 0)
118 return r;
119
120 n_key = 0;
121 value = NULL;
122 n_value = 0;
123
124 } else if (c == '\'')
125 state = SINGLE_QUOTE_VALUE;
126 else if (c == '"')
127 state = DOUBLE_QUOTE_VALUE;
128 else if (c == '\\')
129 state = VALUE_ESCAPE;
130 else if (!strchr(WHITESPACE, c)) {
131 state = VALUE;
132
133 if (!GREEDY_REALLOC(value, n_value+2))
134 return -ENOMEM;
135
136 value[n_value++] = c;
137 }
138
139 break;
140
141 case VALUE:
142 if (strchr(NEWLINE, c)) {
143 state = PRE_KEY;
144 line++;
145
146 key[n_key] = 0;
147
148 if (value)
149 value[n_value] = 0;
150
151 /* Chomp off trailing whitespace from value */
152 if (last_value_whitespace != SIZE_MAX)
153 value[last_value_whitespace] = 0;
154
155 /* strip trailing whitespace from key */
156 if (last_key_whitespace != SIZE_MAX)
157 key[last_key_whitespace] = 0;
158
159 r = push(fname, line, key, value, userdata);
160 if (r < 0)
161 return r;
162
163 n_key = 0;
164 value = NULL;
165 n_value = 0;
166
167 } else if (c == '\\') {
168 state = VALUE_ESCAPE;
169 last_value_whitespace = SIZE_MAX;
170 } else {
171 if (!strchr(WHITESPACE, c))
172 last_value_whitespace = SIZE_MAX;
173 else if (last_value_whitespace == SIZE_MAX)
174 last_value_whitespace = n_value;
175
176 if (!GREEDY_REALLOC(value, n_value+2))
177 return -ENOMEM;
178
179 value[n_value++] = c;
180 }
181
182 break;
183
184 case VALUE_ESCAPE:
185 state = VALUE;
186
187 if (!strchr(NEWLINE, c)) {
188 /* Escaped newlines we eat up entirely */
189 if (!GREEDY_REALLOC(value, n_value+2))
190 return -ENOMEM;
191
192 value[n_value++] = c;
193 }
194 break;
195
196 case SINGLE_QUOTE_VALUE:
197 if (c == '\'')
198 state = PRE_VALUE;
199 else {
200 if (!GREEDY_REALLOC(value, n_value+2))
201 return -ENOMEM;
202
203 value[n_value++] = c;
204 }
205
206 break;
207
208 case DOUBLE_QUOTE_VALUE:
209 if (c == '"')
210 state = PRE_VALUE;
211 else if (c == '\\')
212 state = DOUBLE_QUOTE_VALUE_ESCAPE;
213 else {
214 if (!GREEDY_REALLOC(value, n_value+2))
215 return -ENOMEM;
216
217 value[n_value++] = c;
218 }
219
220 break;
221
222 case DOUBLE_QUOTE_VALUE_ESCAPE:
223 state = DOUBLE_QUOTE_VALUE;
224
225 if (strchr(SHELL_NEED_ESCAPE, c)) {
226 /* If this is a char that needs escaping, just unescape it. */
227 if (!GREEDY_REALLOC(value, n_value+2))
228 return -ENOMEM;
229 value[n_value++] = c;
230 } else if (c != '\n') {
231 /* If other char than what needs escaping, keep the "\" in place, like the
232 * real shell does. */
233 if (!GREEDY_REALLOC(value, n_value+3))
234 return -ENOMEM;
235 value[n_value++] = '\\';
236 value[n_value++] = c;
237 }
238
239 /* Escaped newlines (aka "continuation lines") are eaten up entirely */
240 break;
241
242 case COMMENT:
243 if (c == '\\')
244 state = COMMENT_ESCAPE;
245 else if (strchr(NEWLINE, c)) {
246 state = PRE_KEY;
247 line++;
248 }
249 break;
250
251 case COMMENT_ESCAPE:
252 log_debug("The line which doesn't begin with \";\" or \"#\", but follows a comment" \
253 " line trailing with escape is now treated as a non comment line since v254.");
254 if (strchr(NEWLINE, c)) {
255 state = PRE_KEY;
256 line++;
257 } else
258 state = COMMENT;
259 break;
260 }
261 }
262
263 if (IN_SET(state,
264 PRE_VALUE,
265 VALUE,
266 VALUE_ESCAPE,
267 SINGLE_QUOTE_VALUE,
268 DOUBLE_QUOTE_VALUE,
269 DOUBLE_QUOTE_VALUE_ESCAPE)) {
270
271 key[n_key] = 0;
272
273 if (value)
274 value[n_value] = 0;
275
276 if (state == VALUE)
277 if (last_value_whitespace != SIZE_MAX)
278 value[last_value_whitespace] = 0;
279
280 /* strip trailing whitespace from key */
281 if (last_key_whitespace != SIZE_MAX)
282 key[last_key_whitespace] = 0;
283
284 r = push(fname, line, key, value, userdata);
285 if (r < 0)
286 return r;
287
288 value = NULL;
289 }
290
291 return 0;
292 }
293
294 static int check_utf8ness_and_warn(
295 const char *filename, unsigned line,
296 const char *key, char *value) {
297
298 assert(key);
299
300 if (!utf8_is_valid(key)) {
301 _cleanup_free_ char *p = NULL;
302
303 p = utf8_escape_invalid(key);
304 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
305 "%s:%u: invalid UTF-8 in key '%s', ignoring.",
306 strna(filename), line, p);
307 }
308
309 if (value && !utf8_is_valid(value)) {
310 _cleanup_free_ char *p = NULL;
311
312 p = utf8_escape_invalid(value);
313 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
314 "%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.",
315 strna(filename), line, key, p);
316 }
317
318 return 0;
319 }
320
321 static int parse_env_file_push(
322 const char *filename, unsigned line,
323 const char *key, char *value,
324 void *userdata) {
325
326 const char *k;
327 va_list aq, *ap = userdata;
328 int r;
329
330 assert(key);
331
332 r = check_utf8ness_and_warn(filename, line, key, value);
333 if (r < 0)
334 return r;
335
336 va_copy(aq, *ap);
337
338 while ((k = va_arg(aq, const char *))) {
339 char **v;
340
341 v = va_arg(aq, char **);
342
343 if (streq(key, k)) {
344 va_end(aq);
345 free_and_replace(*v, value);
346
347 return 1;
348 }
349 }
350
351 va_end(aq);
352 free(value);
353
354 return 0;
355 }
356
357 int parse_env_filev(
358 FILE *f,
359 const char *fname,
360 va_list ap) {
361
362 int r;
363 va_list aq;
364
365 assert(f || fname);
366
367 va_copy(aq, ap);
368 r = parse_env_file_internal(f, fname, parse_env_file_push, &aq);
369 va_end(aq);
370 return r;
371 }
372
373 int parse_env_file_fdv(int fd, const char *fname, va_list ap) {
374 _cleanup_fclose_ FILE *f = NULL;
375 va_list aq;
376 int r;
377
378 assert(fd >= 0);
379
380 r = fdopen_independent(fd, "re", &f);
381 if (r < 0)
382 return r;
383
384 va_copy(aq, ap);
385 r = parse_env_file_internal(f, fname, parse_env_file_push, &aq);
386 va_end(aq);
387 return r;
388 }
389
390 int parse_env_file_sentinel(
391 FILE *f,
392 const char *fname,
393 ...) {
394
395 va_list ap;
396 int r;
397
398 assert(f || fname);
399
400 va_start(ap, fname);
401 r = parse_env_filev(f, fname, ap);
402 va_end(ap);
403
404 return r;
405 }
406
407 int parse_env_file_fd_sentinel(
408 int fd,
409 const char *fname, /* only used for logging */
410 ...) {
411
412 va_list ap;
413 int r;
414
415 assert(fd >= 0);
416
417 va_start(ap, fname);
418 r = parse_env_file_fdv(fd, fname, ap);
419 va_end(ap);
420
421 return r;
422 }
423
424 static int load_env_file_push(
425 const char *filename, unsigned line,
426 const char *key, char *value,
427 void *userdata) {
428
429 char ***m = userdata;
430 char *p;
431 int r;
432
433 assert(key);
434
435 r = check_utf8ness_and_warn(filename, line, key, value);
436 if (r < 0)
437 return r;
438
439 p = strjoin(key, "=", value);
440 if (!p)
441 return -ENOMEM;
442
443 r = strv_env_replace_consume(m, p);
444 if (r < 0)
445 return r;
446
447 free(value);
448 return 0;
449 }
450
451 int load_env_file(FILE *f, const char *fname, char ***ret) {
452 _cleanup_strv_free_ char **m = NULL;
453 int r;
454
455 assert(f || fname);
456 assert(ret);
457
458 r = parse_env_file_internal(f, fname, load_env_file_push, &m);
459 if (r < 0)
460 return r;
461
462 *ret = TAKE_PTR(m);
463 return 0;
464 }
465
466 static int load_env_file_push_pairs(
467 const char *filename, unsigned line,
468 const char *key, char *value,
469 void *userdata) {
470
471 char ***m = ASSERT_PTR(userdata);
472 int r;
473
474 assert(key);
475
476 r = check_utf8ness_and_warn(filename, line, key, value);
477 if (r < 0)
478 return r;
479
480 /* Check if the key is present */
481 for (char **t = *m; t && *t; t += 2)
482 if (streq(t[0], key)) {
483 if (value)
484 return free_and_replace(t[1], value);
485 else
486 return free_and_strdup(t+1, "");
487 }
488
489 r = strv_extend(m, key);
490 if (r < 0)
491 return r;
492
493 if (value)
494 return strv_push(m, value);
495 else
496 return strv_extend(m, "");
497 }
498
499 int load_env_file_pairs(FILE *f, const char *fname, char ***ret) {
500 _cleanup_strv_free_ char **m = NULL;
501 int r;
502
503 assert(f || fname);
504 assert(ret);
505
506 r = parse_env_file_internal(f, fname, load_env_file_push_pairs, &m);
507 if (r < 0)
508 return r;
509
510 *ret = TAKE_PTR(m);
511 return 0;
512 }
513
514 int load_env_file_pairs_fd(int fd, const char *fname, char ***ret) {
515 _cleanup_fclose_ FILE *f = NULL;
516 int r;
517
518 assert(fd >= 0);
519
520 r = fdopen_independent(fd, "re", &f);
521 if (r < 0)
522 return r;
523
524 return load_env_file_pairs(f, fname, ret);
525 }
526
527 static int merge_env_file_push(
528 const char *filename, unsigned line,
529 const char *key, char *value,
530 void *userdata) {
531
532 char ***env = ASSERT_PTR(userdata);
533 char *expanded_value;
534 int r;
535
536 assert(key);
537
538 if (!value) {
539 log_error("%s:%u: invalid syntax (around \"%s\"), ignoring.", strna(filename), line, key);
540 return 0;
541 }
542
543 if (!env_name_is_valid(key)) {
544 log_error("%s:%u: invalid variable name \"%s\", ignoring.", strna(filename), line, key);
545 free(value);
546 return 0;
547 }
548
549 r = replace_env(value,
550 *env,
551 REPLACE_ENV_USE_ENVIRONMENT|REPLACE_ENV_ALLOW_BRACELESS|REPLACE_ENV_ALLOW_EXTENDED,
552 &expanded_value);
553 if (r < 0)
554 return log_error_errno(r, "%s:%u: Failed to expand variable '%s': %m", strna(filename), line, value);
555
556 free_and_replace(value, expanded_value);
557
558 log_debug("%s:%u: setting %s=%s", filename, line, key, value);
559
560 return load_env_file_push(filename, line, key, value, env);
561 }
562
563 int merge_env_file(
564 char ***env,
565 FILE *f,
566 const char *fname) {
567
568 assert(env);
569 assert(f || fname);
570
571 /* NOTE: this function supports braceful and braceless variable expansions,
572 * plus "extended" substitutions, unlike other exported parsing functions.
573 */
574
575 return parse_env_file_internal(f, fname, merge_env_file_push, env);
576 }
577
578 static void env_file_fputs_escaped(FILE *f, const char *p) {
579 assert(f);
580 assert(p);
581
582 flockfile(f);
583
584 if (string_has_cc(p, NULL) || chars_intersect(p, WHITESPACE SHELL_NEED_QUOTES)) {
585 fputc_unlocked('"', f);
586
587 for (; *p; p++) {
588 if (strchr(SHELL_NEED_ESCAPE, *p))
589 fputc_unlocked('\\', f);
590
591 fputc_unlocked(*p, f);
592 }
593
594 fputc_unlocked('"', f);
595 } else
596 fputs_unlocked(p, f);
597
598 funlockfile(f);
599 }
600
601 void env_file_fputs_assignment(FILE *f, const char *k, const char *v) {
602 assert(f);
603 assert(k);
604
605 if (!v)
606 return;
607
608 fputs(k, f);
609 env_file_fputs_escaped(f, v);
610 fputc('\n', f);
611 }
612
613 static void write_env_var(FILE *f, const char *v) {
614 const char *p;
615
616 assert(f);
617 assert(v);
618
619 p = strchr(v, '=');
620 if (!p) {
621 /* Fallback */
622 fputs_unlocked(v, f);
623 fputc_unlocked('\n', f);
624 return;
625 }
626
627 p++;
628 fwrite_unlocked(v, 1, p-v, f);
629
630 env_file_fputs_escaped(f, p);
631
632 fputc_unlocked('\n', f);
633 }
634
635 int write_env_file(int dir_fd, const char *fname, char **headers, char **l, WriteEnvFileFlags flags) {
636 _cleanup_fclose_ FILE *f = NULL;
637 _cleanup_free_ char *p = NULL;
638 int r;
639
640 assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
641 assert(fname);
642
643 bool call_label_ops_post = false;
644 if (FLAGS_SET(flags, WRITE_ENV_FILE_LABEL)) {
645 r = label_ops_pre(dir_fd, fname, S_IFREG);
646 if (r < 0)
647 return r;
648
649 call_label_ops_post = true;
650 }
651
652 r = fopen_tmpfile_linkable_at(dir_fd, fname, O_WRONLY|O_CLOEXEC, &p, &f);
653 if (call_label_ops_post)
654 RET_GATHER(r, label_ops_post(f ? fileno(f) : dir_fd, f ? NULL : fname, /* created= */ !!f));
655 if (r < 0)
656 return r;
657
658 r = fchmod_umask(fileno(f), 0644);
659 if (r < 0)
660 goto fail;
661
662 STRV_FOREACH(i, headers) {
663 assert(isempty(*i) || startswith(*i, "#"));
664 fputs_unlocked(*i, f);
665 fputc_unlocked('\n', f);
666 }
667
668 STRV_FOREACH(i, l)
669 write_env_var(f, *i);
670
671 r = flink_tmpfile_at(f, dir_fd, p, fname, LINK_TMPFILE_REPLACE|LINK_TMPFILE_SYNC);
672 if (r < 0)
673 goto fail;
674
675 return 0;
676
677 fail:
678 if (p)
679 (void) unlinkat(dir_fd, p, 0);
680
681 return r;
682 }
683
684 int write_vconsole_conf(int dir_fd, const char *fname, char **l) {
685 char **headers = STRV_MAKE(
686 "# Written by systemd-localed(8) or systemd-firstboot(1), read by systemd-localed",
687 "# and systemd-vconsole-setup(8). Use localectl(1) to update this file.");
688
689 return write_env_file(dir_fd, fname, headers, l, WRITE_ENV_FILE_LABEL);
690 }