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