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