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