/src/systemd/src/basic/conf-files.c
Line | Count | Source |
1 | | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | | |
3 | | #include <stdio.h> |
4 | | #include <stdlib.h> |
5 | | |
6 | | #include "alloc-util.h" |
7 | | #include "chase.h" |
8 | | #include "conf-files.h" |
9 | | #include "dirent-util.h" |
10 | | #include "errno-util.h" |
11 | | #include "fd-util.h" |
12 | | #include "fileio.h" |
13 | | #include "glyph-util.h" |
14 | | #include "hashmap.h" |
15 | | #include "log.h" |
16 | | #include "nulstr-util.h" |
17 | | #include "path-util.h" |
18 | | #include "set.h" |
19 | | #include "stat-util.h" |
20 | | #include "string-util.h" |
21 | | #include "strv.h" |
22 | | |
23 | 7.78k | ConfFile* conf_file_free(ConfFile *c) { |
24 | 7.78k | if (!c) |
25 | 0 | return NULL; |
26 | | |
27 | 7.78k | free(c->filename); |
28 | 7.78k | free(c->result); |
29 | 7.78k | free(c->original_path); |
30 | 7.78k | free(c->resolved_path); |
31 | 7.78k | safe_close(c->fd); |
32 | | |
33 | 7.78k | return mfree(c); |
34 | 7.78k | } |
35 | | |
36 | 0 | DEFINE_POINTER_ARRAY_FREE_FUNC(ConfFile*, conf_file_free); |
37 | | |
38 | 90.1k | static int conf_files_log_level(ConfFilesFlags flags) { |
39 | 90.1k | return FLAGS_SET(flags, CONF_FILES_WARN) ? LOG_WARNING : LOG_DEBUG; |
40 | 90.1k | } |
41 | | |
42 | | static int prepare_dirs( |
43 | | const char *root, |
44 | | ConfFilesFlags flags, |
45 | | char * const *dirs, |
46 | | char **ret_root, |
47 | | int *ret_rfd, |
48 | 7.78k | char ***ret_dirs) { |
49 | | |
50 | 7.78k | _cleanup_free_ char *root_abs = NULL; |
51 | 7.78k | _cleanup_strv_free_ char **dirs_abs = NULL; |
52 | 7.78k | int r; |
53 | | |
54 | 7.78k | assert(ret_root); |
55 | 7.78k | assert(ret_rfd); |
56 | 7.78k | assert(ret_dirs || strv_isempty(dirs)); |
57 | | |
58 | 7.78k | int log_level = conf_files_log_level(flags); |
59 | | |
60 | 7.78k | r = empty_or_root_harder_to_null(&root); |
61 | 7.78k | if (r < 0) |
62 | 0 | return log_full_errno(log_level, r, "Failed to determine if '%s' points to the root directory: %m", strempty(root)); |
63 | | |
64 | 7.78k | if (ret_dirs) { |
65 | 0 | dirs_abs = strv_copy(dirs); |
66 | 0 | if (!dirs_abs) |
67 | 0 | return log_oom_full(log_level); |
68 | 0 | } |
69 | | |
70 | 7.78k | _cleanup_close_ int rfd = XAT_FDROOT; |
71 | 7.78k | if (root) { |
72 | | /* When a non-trivial root is specified, we will prefix the result later. Hence, it is not |
73 | | * necessary to modify each config directories here. but needs to normalize the root directory. */ |
74 | 0 | r = path_make_absolute_cwd(root, &root_abs); |
75 | 0 | if (r < 0) |
76 | 0 | return log_full_errno(log_level, r, "Failed to make '%s' absolute: %m", root); |
77 | | |
78 | 0 | path_simplify(root_abs); |
79 | |
|
80 | 0 | rfd = open(root, O_CLOEXEC|O_DIRECTORY|O_PATH); |
81 | 0 | if (rfd < 0) |
82 | 0 | return log_full_errno(log_level, errno, "Failed to open '%s': %m", root_abs); |
83 | |
|
84 | 7.78k | } else if (ret_dirs) { |
85 | | /* When an empty root or "/" is specified, we will open "/" below, hence we need to make |
86 | | * each config directory absolute if relative. */ |
87 | 0 | r = path_strv_make_absolute_cwd(dirs_abs); |
88 | 0 | if (r < 0) |
89 | 0 | return log_full_errno(log_level, r, "Failed to make directories absolute: %m"); |
90 | 0 | } |
91 | | |
92 | 7.78k | *ret_root = TAKE_PTR(root_abs); |
93 | 7.78k | *ret_rfd = TAKE_FD(rfd); |
94 | 7.78k | if (ret_dirs) |
95 | 0 | *ret_dirs = TAKE_PTR(dirs_abs); |
96 | 7.78k | return 0; |
97 | 7.78k | } |
98 | | |
99 | 7.78k | static int conf_file_prefix_root(ConfFile *c, const char *root, ConfFilesFlags flags) { |
100 | 7.78k | char *p; |
101 | 7.78k | int r; |
102 | | |
103 | 7.78k | assert(c); |
104 | | |
105 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_DONT_PREFIX_ROOT)) |
106 | 0 | return 0; |
107 | | |
108 | 7.78k | int log_level = conf_files_log_level(flags); |
109 | | |
110 | 7.78k | r = chaseat_prefix_root(c->result, root, &p); |
111 | 7.78k | if (r < 0) |
112 | 0 | return log_full_errno(log_level, r, "Failed to prefix '%s' with root '%s': %m", c->result, root); |
113 | 7.78k | free_and_replace(c->result, p); |
114 | | |
115 | 7.78k | r = chaseat_prefix_root(c->resolved_path, root, &p); |
116 | 7.78k | if (r < 0) |
117 | 0 | return log_full_errno(log_level, r, "Failed to prefix '%s' with root '%s': %m", c->resolved_path, root); |
118 | 7.78k | free_and_replace(c->resolved_path, p); |
119 | | |
120 | | /* Do not use chaseat_prefix_root(), as it is for the result of chaseat(), but the path is not chased. */ |
121 | 7.78k | p = path_join(empty_to_root(root), skip_leading_slash(c->original_path)); |
122 | 7.78k | if (!p) |
123 | 0 | return log_oom_full(log_level); |
124 | | |
125 | 7.78k | path_simplify(p); |
126 | 7.78k | return free_and_replace(c->original_path, p); |
127 | 7.78k | } |
128 | | |
129 | 15.5k | static bool conf_files_need_stat(ConfFilesFlags flags) { |
130 | 15.5k | return (flags & (CONF_FILES_FILTER_MASKED | CONF_FILES_REGULAR | CONF_FILES_DIRECTORY | CONF_FILES_EXECUTABLE)) != 0; |
131 | 15.5k | } |
132 | | |
133 | 15.5k | static ChaseFlags conf_files_chase_flags(ConfFilesFlags flags) { |
134 | 15.5k | ChaseFlags chase_flags = 0; |
135 | | |
136 | 15.5k | if (!conf_files_need_stat(flags) || FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK)) |
137 | | /* Even if no verification is requested, let's unconditionally call chaseat(), |
138 | | * to drop unsafe symlinks. */ |
139 | 0 | chase_flags |= CHASE_NONEXISTENT; |
140 | | |
141 | 15.5k | return chase_flags; |
142 | 15.5k | } |
143 | | |
144 | | static int conf_file_chase_and_verify( |
145 | | const char *root, /* for logging, can be NULL */ |
146 | | int rfd, |
147 | | const char *original_path, /* for logging */ |
148 | | const char *path, |
149 | | const char *name, |
150 | | Set **masked, /* optional */ |
151 | | ConfFilesFlags flags, |
152 | | char **ret_path, |
153 | | int *ret_fd, |
154 | 7.78k | struct stat *ret_stat) { |
155 | | |
156 | 7.78k | _cleanup_free_ char *resolved_path = NULL; |
157 | 7.78k | _cleanup_close_ int fd = -EBADF; |
158 | 7.78k | struct stat st = {}; |
159 | 7.78k | int r; |
160 | | |
161 | 7.78k | assert(wildcard_fd_is_valid(rfd)); |
162 | 7.78k | assert(original_path); |
163 | 7.78k | assert(path); |
164 | 7.78k | assert(name); |
165 | | |
166 | 7.78k | int log_level = conf_files_log_level(flags); |
167 | | |
168 | 7.78k | root = empty_to_root(root); |
169 | | |
170 | 7.78k | r = chaseat(rfd, rfd, path, conf_files_chase_flags(flags), &resolved_path, &fd); |
171 | 7.78k | if (r < 0) |
172 | 0 | return log_full_errno(log_level, r, "Failed to chase '%s%s': %m", |
173 | 7.78k | root, skip_leading_slash(original_path)); |
174 | 7.78k | if (r == 0) { |
175 | 0 | if (FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK)) { |
176 | | /* If the path points to /dev/null in a image or so, then the device node may not exist. */ |
177 | 0 | if (path_equal(skip_leading_slash(resolved_path), "dev/null")) { |
178 | 0 | if (masked) { |
179 | | /* Mark this one as masked */ |
180 | 0 | r = set_put_strdup(masked, name); |
181 | 0 | if (r < 0) |
182 | 0 | return log_oom_full(log_level); |
183 | 0 | } |
184 | | |
185 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), |
186 | 0 | "File '%s%s' is a mask (symlink to /dev/null).", |
187 | 0 | root, skip_leading_slash(original_path)); |
188 | 0 | } |
189 | 0 | } |
190 | | |
191 | 0 | if (conf_files_need_stat(flags)) |
192 | | /* If we need to have stat, skip the entry. */ |
193 | 0 | return log_full_errno(log_level, SYNTHETIC_ERRNO(ENOENT), "Failed to chase '%s%s': %m", |
194 | 0 | root, skip_leading_slash(original_path)); |
195 | 0 | } |
196 | | |
197 | | /* Even if we do not need stat, let's take stat now. The caller may use the info later. */ |
198 | 7.78k | if (fd >= 0 && fstat(fd, &st) < 0) |
199 | 0 | return log_full_errno(log_level, errno, "Failed to stat '%s%s': %m", |
200 | 7.78k | root, skip_leading_slash(original_path)); |
201 | | |
202 | | /* Is this a masking entry? */ |
203 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_SYMLINK) && stat_may_be_dev_null(&st)) { |
204 | 0 | if (masked) { |
205 | | /* Mark this one as masked */ |
206 | 0 | r = set_put_strdup(masked, name); |
207 | 0 | if (r < 0) |
208 | 0 | return log_oom_full(log_level); |
209 | 0 | } |
210 | | |
211 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), |
212 | 0 | "File '%s%s' is a mask (symlink to /dev/null).", |
213 | 0 | root, skip_leading_slash(original_path)); |
214 | 0 | } |
215 | | |
216 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_FILTER_MASKED_BY_EMPTY) && stat_is_empty(&st)) { |
217 | 0 | if (masked) { |
218 | | /* Mark this one as masked */ |
219 | 0 | r = set_put_strdup(masked, name); |
220 | 0 | if (r < 0) |
221 | 0 | return log_oom_full(log_level); |
222 | 0 | } |
223 | | |
224 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), |
225 | 0 | "File '%s%s' is a mask (an empty file).", |
226 | 0 | root, skip_leading_slash(original_path)); |
227 | 0 | } |
228 | | |
229 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_REGULAR|CONF_FILES_DIRECTORY)) { |
230 | 0 | if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) |
231 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(EBADFD), |
232 | 0 | "File '%s%s' is neither a regular file or directory.", |
233 | 0 | root, skip_leading_slash(original_path)); |
234 | 7.78k | } else { |
235 | | /* Is this node a regular file? */ |
236 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_REGULAR)) { |
237 | 7.78k | r = stat_verify_regular(&st); |
238 | 7.78k | if (r < 0) |
239 | 0 | return log_debug_errno(r, "File '%s%s' is not a regular file: %m", |
240 | 7.78k | root, skip_leading_slash(original_path)); |
241 | 7.78k | } |
242 | | |
243 | | /* Is this node a directory? */ |
244 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_DIRECTORY)) { |
245 | 0 | r = stat_verify_directory(&st); |
246 | 0 | if (r < 0) |
247 | 0 | return log_debug_errno(r, "File '%s%s' is not a directory: %m", |
248 | 0 | root, skip_leading_slash(original_path)); |
249 | 0 | } |
250 | 7.78k | } |
251 | | |
252 | | /* Does this node have the executable bit set? |
253 | | * As requested: check if the file is marked executable. Note that we don't check access(X_OK) here, |
254 | | * as we care about whether the file is marked executable at all, and not whether it is executable |
255 | | * for us, because if so, such errors are stuff we should log about. */ |
256 | 7.78k | if (FLAGS_SET(flags, CONF_FILES_EXECUTABLE) && (st.st_mode & 0111) == 0) |
257 | 0 | return log_debug_errno(SYNTHETIC_ERRNO(ENOEXEC), |
258 | 7.78k | "File '%s%s' is not marked executable.", |
259 | 7.78k | root, skip_leading_slash(original_path)); |
260 | | |
261 | 7.78k | if (ret_path) |
262 | 7.78k | *ret_path = TAKE_PTR(resolved_path); |
263 | 7.78k | if (ret_fd) |
264 | 7.78k | *ret_fd = TAKE_FD(fd); |
265 | 7.78k | if (ret_stat) |
266 | 7.78k | *ret_stat = st; |
267 | | |
268 | 7.78k | return 0; |
269 | 7.78k | } |
270 | | |
271 | | int conf_file_new_at( |
272 | | const char *path, |
273 | | const char *root, |
274 | | int rfd, |
275 | | ConfFilesFlags flags, |
276 | 7.78k | ConfFile **ret) { |
277 | 7.78k | int r; |
278 | | |
279 | 7.78k | assert(path); |
280 | 7.78k | assert(wildcard_fd_is_valid(rfd)); |
281 | 7.78k | assert(ret); |
282 | | |
283 | 7.78k | int log_level = conf_files_log_level(flags); |
284 | | |
285 | 7.78k | _cleanup_free_ char *_root = NULL; |
286 | 7.78k | if (DEBUG_LOGGING && !root) { |
287 | 0 | (void) fd_get_path(rfd, &_root); |
288 | 0 | root = _root; |
289 | 0 | } |
290 | | |
291 | 7.78k | _cleanup_(conf_file_freep) ConfFile *c = new(ConfFile, 1); |
292 | 7.78k | if (!c) |
293 | 0 | return log_oom_full(log_level); |
294 | | |
295 | 7.78k | *c = (ConfFile) { |
296 | 7.78k | .original_path = strdup(path), |
297 | 7.78k | .fd = -EBADF, |
298 | 7.78k | }; |
299 | | |
300 | 7.78k | if (!c->original_path) |
301 | 0 | return log_oom_full(log_level); |
302 | | |
303 | 7.78k | r = path_extract_filename(path, &c->filename); |
304 | 7.78k | if (r < 0) |
305 | 0 | return log_full_errno(log_level, r, "Failed to extract filename from '%s': %m", path); |
306 | | |
307 | 7.78k | _cleanup_free_ char *dirpath = NULL, *resolved_dirpath = NULL; |
308 | 7.78k | r = path_extract_directory(path, &dirpath); |
309 | 7.78k | if (r < 0 && r != -EDESTADDRREQ) |
310 | 0 | return log_full_errno(log_level, r, "Failed to extract directory from '%s': %m", path); |
311 | 7.78k | if (r >= 0) { |
312 | 7.78k | r = chaseat(rfd, rfd, dirpath, |
313 | 7.78k | CHASE_MUST_BE_DIRECTORY | conf_files_chase_flags(flags), |
314 | 7.78k | &resolved_dirpath, /* ret_fd= */ NULL); |
315 | 7.78k | if (r < 0) |
316 | 0 | return log_full_errno(log_level, r, "Failed to chase '%s%s': %m", empty_to_root(root), skip_leading_slash(dirpath)); |
317 | 7.78k | } |
318 | | |
319 | 7.78k | c->result = path_join(resolved_dirpath, c->filename); |
320 | 7.78k | if (!c->result) |
321 | 0 | return log_oom_full(log_level); |
322 | | |
323 | 7.78k | r = conf_file_chase_and_verify( |
324 | 7.78k | root, |
325 | 7.78k | rfd, |
326 | 7.78k | c->original_path, |
327 | 7.78k | c->result, |
328 | 7.78k | c->filename, |
329 | 7.78k | /* masked= */ NULL, |
330 | 7.78k | flags, |
331 | 7.78k | &c->resolved_path, |
332 | 7.78k | &c->fd, |
333 | 7.78k | &c->st); |
334 | 7.78k | if (r < 0) |
335 | 0 | return r; |
336 | | |
337 | 7.78k | *ret = TAKE_PTR(c); |
338 | 7.78k | return 0; |
339 | 7.78k | } |
340 | | |
341 | 7.78k | int conf_file_new(const char *path, const char *root, ConfFilesFlags flags, ConfFile **ret) { |
342 | 7.78k | int r; |
343 | | |
344 | 7.78k | assert(path); |
345 | 7.78k | assert(ret); |
346 | | |
347 | 7.78k | _cleanup_free_ char *root_abs = NULL; |
348 | 7.78k | _cleanup_close_ int rfd = -EBADF; |
349 | 7.78k | r = prepare_dirs(root, flags, /* dirs= */ NULL, &root_abs, &rfd, /* ret_dirs= */ NULL); |
350 | 7.78k | if (r < 0) |
351 | 0 | return r; |
352 | | |
353 | 7.78k | _cleanup_free_ char *path_abs = NULL; |
354 | 7.78k | if (!root_abs) { |
355 | 7.78k | r = path_make_absolute_cwd(path, &path_abs); |
356 | 7.78k | if (r < 0) |
357 | 0 | return log_full_errno(conf_files_log_level(flags), r, "Failed to make '%s' absolute: %m", path); |
358 | | |
359 | 7.78k | path = path_abs; |
360 | 7.78k | } |
361 | | |
362 | 7.78k | _cleanup_(conf_file_freep) ConfFile *c = NULL; |
363 | 7.78k | r = conf_file_new_at(path, root_abs, rfd, flags, &c); |
364 | 7.78k | if (r < 0) |
365 | 0 | return r; |
366 | | |
367 | 7.78k | r = conf_file_prefix_root(c, root_abs, flags); |
368 | 7.78k | if (r < 0) |
369 | 0 | return r; |
370 | | |
371 | 7.78k | *ret = TAKE_PTR(c); |
372 | 7.78k | return 0; |
373 | 7.78k | } |
374 | | |
375 | | DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( |
376 | | conf_file_hash_ops, |
377 | | char, string_hash_func, string_compare_func, |
378 | | ConfFile, conf_file_free); |
379 | | |
380 | | static int files_add( |
381 | | DIR *dir, |
382 | | const char *original_dirpath, |
383 | | const char *resolved_dirpath, |
384 | | const char *root, /* for logging, can be NULL */ |
385 | | int rfd, |
386 | | Hashmap **files, |
387 | | Set **masked, |
388 | | const char *suffix, |
389 | 0 | ConfFilesFlags flags) { |
390 | |
|
391 | 0 | int r; |
392 | |
|
393 | 0 | assert(dir); |
394 | 0 | assert(original_dirpath); |
395 | 0 | assert(resolved_dirpath); |
396 | 0 | assert(wildcard_fd_is_valid(rfd)); |
397 | 0 | assert(files); |
398 | 0 | assert(masked); |
399 | |
|
400 | 0 | int log_level = conf_files_log_level(flags); |
401 | |
|
402 | 0 | root = empty_to_root(root); |
403 | |
|
404 | 0 | FOREACH_DIRENT(de, dir, return log_full_errno(log_level, errno, "Failed to read directory '%s%s': %m", |
405 | 0 | root, skip_leading_slash(original_dirpath))) { |
406 | |
|
407 | 0 | _cleanup_free_ char *original_path = path_join(original_dirpath, de->d_name); |
408 | 0 | if (!original_path) |
409 | 0 | return log_oom_full(log_level); |
410 | | |
411 | | /* Does this match the suffix? */ |
412 | 0 | if (suffix && !endswith(de->d_name, suffix)) { |
413 | 0 | log_debug("Skipping file '%s%s', suffix is not '%s'.", root, skip_leading_slash(original_path), suffix); |
414 | 0 | continue; |
415 | 0 | } |
416 | | |
417 | | /* Has this file already been found in an earlier directory? */ |
418 | 0 | if (hashmap_contains(*files, de->d_name)) { |
419 | 0 | log_debug("Skipping overridden file '%s%s'.", root, skip_leading_slash(original_path)); |
420 | 0 | continue; |
421 | 0 | } |
422 | | |
423 | | /* Has this been masked in an earlier directory? */ |
424 | 0 | if ((flags & CONF_FILES_FILTER_MASKED) != 0 && set_contains(*masked, de->d_name)) { |
425 | 0 | log_debug("File '%s%s' is masked by previous entry.", root, skip_leading_slash(original_path)); |
426 | 0 | continue; |
427 | 0 | } |
428 | | |
429 | 0 | _cleanup_free_ char *p = path_join(resolved_dirpath, de->d_name); |
430 | 0 | if (!p) |
431 | 0 | return log_oom_full(log_level); |
432 | | |
433 | 0 | _cleanup_free_ char *resolved_path = NULL; |
434 | 0 | _cleanup_close_ int fd = -EBADF; |
435 | 0 | struct stat st; |
436 | 0 | r = conf_file_chase_and_verify( |
437 | 0 | root, |
438 | 0 | rfd, |
439 | 0 | original_path, |
440 | 0 | p, |
441 | 0 | de->d_name, |
442 | 0 | masked, |
443 | 0 | flags, |
444 | 0 | &resolved_path, |
445 | 0 | &fd, |
446 | 0 | &st); |
447 | 0 | if (r == -ENOMEM) |
448 | 0 | return r; |
449 | 0 | if (r < 0) |
450 | 0 | continue; |
451 | | |
452 | 0 | _cleanup_(conf_file_freep) ConfFile *c = new(ConfFile, 1); |
453 | 0 | if (!c) |
454 | 0 | return log_oom_full(log_level); |
455 | | |
456 | 0 | *c = (ConfFile) { |
457 | 0 | .filename = strdup(de->d_name), |
458 | 0 | .result = TAKE_PTR(p), |
459 | 0 | .original_path = TAKE_PTR(original_path), |
460 | 0 | .resolved_path = TAKE_PTR(resolved_path), |
461 | 0 | .fd = TAKE_FD(fd), |
462 | 0 | .st = st, |
463 | 0 | }; |
464 | |
|
465 | 0 | if (!c->filename) |
466 | 0 | return log_oom_full(log_level); |
467 | | |
468 | 0 | r = hashmap_ensure_put(files, &conf_file_hash_ops, c->filename, c); |
469 | 0 | if (r < 0) { |
470 | 0 | assert(r == -ENOMEM); |
471 | 0 | return log_oom_full(log_level); |
472 | 0 | } |
473 | 0 | assert(r > 0); |
474 | |
|
475 | 0 | TAKE_PTR(c); |
476 | 0 | } |
477 | | |
478 | 0 | return 0; |
479 | 0 | } |
480 | | |
481 | 0 | static int dump_files(Hashmap *fh, const char *root, ConfFilesFlags flags, ConfFile ***ret_files, size_t *ret_n_files) { |
482 | 0 | ConfFile **files = NULL; |
483 | 0 | size_t n_files = 0; |
484 | 0 | int r; |
485 | |
|
486 | 0 | CLEANUP_ARRAY(files, n_files, conf_file_free_array); |
487 | |
|
488 | 0 | assert(ret_files); |
489 | 0 | assert(ret_n_files); |
490 | | |
491 | | /* The entries in the array given by hashmap_dump_sorted() are still owned by the hashmap. */ |
492 | 0 | r = hashmap_dump_sorted(fh, (void***) &files, &n_files); |
493 | 0 | if (r < 0) |
494 | 0 | return log_oom_full(conf_files_log_level(flags)); |
495 | | |
496 | | /* Hence, we need to remove them from the hashmap. */ |
497 | 0 | FOREACH_ARRAY(i, files, n_files) |
498 | 0 | assert_se(hashmap_remove(fh, (*i)->filename) == *i); |
499 | |
|
500 | 0 | if (root) |
501 | 0 | FOREACH_ARRAY(i, files, n_files) { |
502 | 0 | r = conf_file_prefix_root(*i, root, flags); |
503 | 0 | if (r < 0) |
504 | 0 | return r; |
505 | 0 | } |
506 | | |
507 | 0 | *ret_files = TAKE_PTR(files); |
508 | 0 | *ret_n_files = n_files; |
509 | 0 | return 0; |
510 | 0 | } |
511 | | |
512 | | static int copy_and_sort_files_from_hashmap( |
513 | | Hashmap *fh, |
514 | | const char *suffix, |
515 | | const char *root, |
516 | | ConfFilesFlags flags, |
517 | 58.9k | char ***ret) { |
518 | | |
519 | 58.9k | _cleanup_strv_free_ char **results = NULL; |
520 | 58.9k | _cleanup_free_ ConfFile **files = NULL; |
521 | 58.9k | size_t n_files = 0, n_results = 0; |
522 | 58.9k | int r; |
523 | | |
524 | 58.9k | assert(ret); |
525 | | |
526 | 58.9k | int log_level = conf_files_log_level(flags); |
527 | | |
528 | | /* The entries in the array given by hashmap_dump_sorted() are still owned by the hashmap. |
529 | | * Hence, do not use conf_file_free_array() for 'entries' */ |
530 | 58.9k | r = hashmap_dump_sorted(fh, (void***) &files, &n_files); |
531 | 58.9k | if (r < 0) |
532 | 0 | return log_oom_full(log_level); |
533 | | |
534 | 58.9k | FOREACH_ARRAY(i, files, n_files) { |
535 | 0 | ConfFile *c = *i; |
536 | 0 | const char *add = NULL; |
537 | |
|
538 | 0 | if (FLAGS_SET(flags, CONF_FILES_BASENAME)) |
539 | 0 | add = c->filename; |
540 | 0 | else if (root && !FLAGS_SET(flags, CONF_FILES_DONT_PREFIX_ROOT)) { |
541 | 0 | _cleanup_free_ char *p = NULL; |
542 | |
|
543 | 0 | r = chaseat_prefix_root(c->result, root, &p); |
544 | 0 | if (r < 0) |
545 | 0 | return log_full_errno(log_level, r, "Failed to prefix '%s' with root '%s': %m", c->result, root); |
546 | | |
547 | 0 | if (FLAGS_SET(flags, CONF_FILES_TRUNCATE_SUFFIX) && suffix) { |
548 | 0 | char *e = endswith(p, suffix); |
549 | 0 | if (!e) |
550 | 0 | continue; |
551 | | |
552 | 0 | *e = 0; |
553 | 0 | } |
554 | | |
555 | 0 | if (strv_consume_with_size(&results, &n_results, TAKE_PTR(p)) < 0) |
556 | 0 | return log_oom_full(log_level); |
557 | | |
558 | 0 | continue; |
559 | 0 | } else |
560 | 0 | add = c->result; |
561 | | |
562 | 0 | if (FLAGS_SET(flags, CONF_FILES_TRUNCATE_SUFFIX)) { |
563 | 0 | const char *e = endswith(add, suffix); |
564 | 0 | if (!e) |
565 | 0 | continue; |
566 | | |
567 | 0 | _cleanup_free_ char *n = strndup(add, e - add); |
568 | 0 | if (!n) |
569 | 0 | return log_oom_full(log_level); |
570 | | |
571 | 0 | r = strv_consume_with_size(&results, &n_results, TAKE_PTR(n)); |
572 | 0 | } else |
573 | 0 | r = strv_extend_with_size(&results, &n_results, add); |
574 | 0 | if (r < 0) |
575 | 0 | return log_oom_full(log_level); |
576 | 0 | } |
577 | | |
578 | 58.9k | *ret = TAKE_PTR(results); |
579 | 58.9k | return 0; |
580 | 58.9k | } |
581 | | |
582 | 0 | static int insert_replacement(Hashmap **fh, ConfFile *replacement, ConfFilesFlags flags, const ConfFile **ret) { |
583 | 0 | _cleanup_(conf_file_freep) ConfFile *c = ASSERT_PTR(replacement); |
584 | 0 | int r; |
585 | |
|
586 | 0 | assert(fh); |
587 | 0 | assert(ret); |
588 | | |
589 | | /* This consumes the input ConfFile. */ |
590 | |
|
591 | 0 | ConfFile *existing = hashmap_get(*fh, c->filename); |
592 | 0 | if (existing) { |
593 | 0 | log_debug("An entry with higher priority '%s' -> '%s' already exists, ignoring the replacement: %s", |
594 | 0 | existing->filename, existing->result, c->original_path); |
595 | 0 | *ret = NULL; |
596 | 0 | return 0; |
597 | 0 | } |
598 | | |
599 | 0 | r = hashmap_ensure_put(fh, &conf_file_hash_ops, c->filename, c); |
600 | 0 | if (r < 0) { |
601 | 0 | assert(r == -ENOMEM); |
602 | 0 | return log_oom_full(conf_files_log_level(flags)); |
603 | 0 | } |
604 | 0 | assert(r > 0); |
605 | |
|
606 | 0 | log_debug("Inserted replacement: '%s' -> '%s'", c->filename, c->result); |
607 | |
|
608 | 0 | *ret = TAKE_PTR(c); |
609 | 0 | return 0; |
610 | 0 | } |
611 | | |
612 | | static int conf_files_list_impl( |
613 | | const char *suffix, |
614 | | const char *root, /* for logging, can be NULL */ |
615 | | int rfd, |
616 | | ConfFilesFlags flags, |
617 | | const char * const *dirs, |
618 | | const char *replacement, |
619 | | Hashmap **ret, |
620 | 58.9k | const ConfFile **ret_inserted) { |
621 | | |
622 | 58.9k | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
623 | 58.9k | _cleanup_set_free_ Set *masked = NULL; |
624 | 58.9k | _cleanup_(conf_file_freep) ConfFile *c = NULL; |
625 | 58.9k | const ConfFile *inserted = NULL; |
626 | 58.9k | int r; |
627 | | |
628 | 58.9k | assert(wildcard_fd_is_valid(rfd)); |
629 | 58.9k | assert(ret); |
630 | | |
631 | 58.9k | root = empty_to_root(root); |
632 | | |
633 | 58.9k | if (replacement) { |
634 | 0 | r = conf_file_new_at(replacement, root, rfd, flags & CONF_FILES_WARN, &c); |
635 | 0 | if (r < 0) |
636 | 0 | return r; |
637 | 0 | } |
638 | | |
639 | 235k | STRV_FOREACH(p, dirs) { |
640 | 235k | _cleanup_closedir_ DIR *dir = NULL; |
641 | 235k | _cleanup_free_ char *path = NULL; |
642 | | |
643 | 235k | r = chase_and_opendirat(rfd, rfd, *p, 0, &path, &dir); |
644 | 235k | if (r < 0) { |
645 | 235k | if (r != -ENOENT) |
646 | 0 | log_full_errno(conf_files_log_level(flags), r, |
647 | 235k | "Failed to chase and open directory '%s%s', ignoring: %m", |
648 | 235k | root, skip_leading_slash(*p)); |
649 | 235k | continue; |
650 | 235k | } |
651 | | |
652 | 0 | if (c && streq_ptr(path_startswith(c->result, path), c->filename)) { |
653 | 0 | r = insert_replacement(&fh, TAKE_PTR(c), flags, &inserted); |
654 | 0 | if (r < 0) |
655 | 0 | return r; |
656 | 0 | } |
657 | | |
658 | 0 | r = files_add(dir, *p, path, root, rfd, &fh, &masked, suffix, flags); |
659 | 0 | if (r == -ENOMEM) |
660 | 0 | return r; |
661 | 0 | } |
662 | | |
663 | 58.9k | if (c) { |
664 | 0 | r = insert_replacement(&fh, TAKE_PTR(c), flags, &inserted); |
665 | 0 | if (r < 0) |
666 | 0 | return r; |
667 | 0 | } |
668 | | |
669 | 58.9k | *ret = TAKE_PTR(fh); |
670 | 58.9k | if (ret_inserted) |
671 | 0 | *ret_inserted = inserted; |
672 | 58.9k | return 0; |
673 | 58.9k | } |
674 | | |
675 | | int conf_files_list_strv( |
676 | | char ***ret, |
677 | | const char *suffix, |
678 | | const char *root, |
679 | | ConfFilesFlags flags, |
680 | 0 | const char * const *dirs) { |
681 | |
|
682 | 0 | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
683 | 0 | _cleanup_close_ int rfd = -EBADF; |
684 | 0 | _cleanup_free_ char *root_abs = NULL; |
685 | 0 | _cleanup_strv_free_ char **dirs_abs = NULL; |
686 | 0 | int r; |
687 | |
|
688 | 0 | assert(ret); |
689 | |
|
690 | 0 | r = prepare_dirs(root, flags, (char**) dirs, &root_abs, &rfd, &dirs_abs); |
691 | 0 | if (r < 0) |
692 | 0 | return r; |
693 | | |
694 | 0 | r = conf_files_list_impl(suffix, root_abs, rfd, flags, (const char * const *) dirs_abs, |
695 | 0 | /* replacement= */ NULL, &fh, /* ret_inserted= */ NULL); |
696 | 0 | if (r < 0) |
697 | 0 | return r; |
698 | | |
699 | 0 | return copy_and_sort_files_from_hashmap(fh, suffix, empty_to_root(root_abs), flags, ret); |
700 | 0 | } |
701 | | |
702 | | int conf_files_list_strv_full( |
703 | | const char *suffix, |
704 | | const char *root, |
705 | | ConfFilesFlags flags, |
706 | | const char * const *dirs, |
707 | | ConfFile ***ret_files, |
708 | 0 | size_t *ret_n_files) { |
709 | |
|
710 | 0 | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
711 | 0 | _cleanup_close_ int rfd = -EBADF; |
712 | 0 | _cleanup_free_ char *root_abs = NULL; |
713 | 0 | _cleanup_strv_free_ char **dirs_abs = NULL; |
714 | 0 | int r; |
715 | |
|
716 | 0 | assert(ret_files); |
717 | 0 | assert(ret_n_files); |
718 | |
|
719 | 0 | r = prepare_dirs(root, flags, (char**) dirs, &root_abs, &rfd, &dirs_abs); |
720 | 0 | if (r < 0) |
721 | 0 | return r; |
722 | | |
723 | 0 | r = conf_files_list_impl(suffix, root_abs, rfd, flags, (const char * const *) dirs_abs, |
724 | 0 | /* replacement= */ NULL, &fh, /* ret_inserted= */ NULL); |
725 | 0 | if (r < 0) |
726 | 0 | return r; |
727 | | |
728 | 0 | return dump_files(fh, empty_to_root(root_abs), flags, ret_files, ret_n_files); |
729 | 0 | } |
730 | | |
731 | | int conf_files_list_strv_at( |
732 | | char ***ret, |
733 | | const char *suffix, |
734 | | int rfd, |
735 | | ConfFilesFlags flags, |
736 | 58.9k | const char * const *dirs) { |
737 | | |
738 | 58.9k | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
739 | 58.9k | _cleanup_free_ char *root = NULL; |
740 | 58.9k | int r; |
741 | | |
742 | 58.9k | assert(wildcard_fd_is_valid(rfd)); |
743 | 58.9k | assert(ret); |
744 | | |
745 | 58.9k | if (DEBUG_LOGGING) |
746 | 0 | (void) fd_get_path(rfd, &root); /* for logging */ |
747 | | |
748 | 58.9k | r = conf_files_list_impl(suffix, root, rfd, flags, dirs, /* replacement= */ NULL, &fh, /* ret_inserted= */ NULL); |
749 | 58.9k | if (r < 0) |
750 | 0 | return r; |
751 | | |
752 | 58.9k | return copy_and_sort_files_from_hashmap(fh, suffix, /* root= */ NULL, flags, ret); |
753 | 58.9k | } |
754 | | |
755 | | int conf_files_list_strv_at_full( |
756 | | const char *suffix, |
757 | | int rfd, |
758 | | ConfFilesFlags flags, |
759 | | const char * const *dirs, |
760 | | ConfFile ***ret_files, |
761 | 0 | size_t *ret_n_files) { |
762 | |
|
763 | 0 | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
764 | 0 | _cleanup_free_ char *root = NULL; |
765 | 0 | int r; |
766 | |
|
767 | 0 | assert(wildcard_fd_is_valid(rfd)); |
768 | 0 | assert(ret_files); |
769 | 0 | assert(ret_n_files); |
770 | |
|
771 | 0 | if (DEBUG_LOGGING) |
772 | 0 | (void) fd_get_path(rfd, &root); /* for logging */ |
773 | |
|
774 | 0 | r = conf_files_list_impl(suffix, root, rfd, flags, dirs, /* replacement= */ NULL, &fh, /* ret_inserted= */ NULL); |
775 | 0 | if (r < 0) |
776 | 0 | return r; |
777 | | |
778 | 0 | return dump_files(fh, /* root= */ NULL, flags, ret_files, ret_n_files); |
779 | 0 | } |
780 | | |
781 | 0 | int conf_files_list(char ***ret, const char *suffix, const char *root, ConfFilesFlags flags, const char *dir) { |
782 | 0 | return conf_files_list_strv(ret, suffix, root, flags, STRV_MAKE_CONST(dir)); |
783 | 0 | } |
784 | | |
785 | 0 | int conf_files_list_full(const char *suffix, const char *root, ConfFilesFlags flags, const char *dir, ConfFile ***ret_files, size_t *ret_n_files) { |
786 | 0 | return conf_files_list_strv_full(suffix, root, flags, STRV_MAKE_CONST(dir), ret_files, ret_n_files); |
787 | 0 | } |
788 | | |
789 | 0 | int conf_files_list_at(char ***ret, const char *suffix, int rfd, ConfFilesFlags flags, const char *dir) { |
790 | 0 | return conf_files_list_strv_at(ret, suffix, rfd, flags, STRV_MAKE_CONST(dir)); |
791 | 0 | } |
792 | | |
793 | 0 | int conf_files_list_at_full(const char *suffix, int rfd, ConfFilesFlags flags, const char *dir, ConfFile ***ret_files, size_t *ret_n_files) { |
794 | 0 | return conf_files_list_strv_at_full(suffix, rfd, flags, STRV_MAKE_CONST(dir), ret_files, ret_n_files); |
795 | 0 | } |
796 | | |
797 | 0 | int conf_files_list_nulstr(char ***ret, const char *suffix, const char *root, ConfFilesFlags flags, const char *dirs) { |
798 | 0 | _cleanup_strv_free_ char **d = NULL; |
799 | |
|
800 | 0 | assert(ret); |
801 | |
|
802 | 0 | d = strv_split_nulstr(dirs); |
803 | 0 | if (!d) |
804 | 0 | return log_oom_full(conf_files_log_level(flags)); |
805 | | |
806 | 0 | return conf_files_list_strv(ret, suffix, root, flags, (const char**) d); |
807 | 0 | } |
808 | | |
809 | 0 | int conf_files_list_nulstr_full(const char *suffix, const char *root, ConfFilesFlags flags, const char *dirs, ConfFile ***ret_files, size_t *ret_n_files) { |
810 | 0 | _cleanup_strv_free_ char **d = NULL; |
811 | |
|
812 | 0 | assert(ret_files); |
813 | 0 | assert(ret_n_files); |
814 | |
|
815 | 0 | d = strv_split_nulstr(dirs); |
816 | 0 | if (!d) |
817 | 0 | return log_oom_full(conf_files_log_level(flags)); |
818 | | |
819 | 0 | return conf_files_list_strv_full(suffix, root, flags, (const char**) d, ret_files, ret_n_files); |
820 | 0 | } |
821 | | |
822 | 0 | int conf_files_list_nulstr_at(char ***ret, const char *suffix, int rfd, ConfFilesFlags flags, const char *dirs) { |
823 | 0 | _cleanup_strv_free_ char **d = NULL; |
824 | |
|
825 | 0 | assert(ret); |
826 | |
|
827 | 0 | d = strv_split_nulstr(dirs); |
828 | 0 | if (!d) |
829 | 0 | return log_oom_full(conf_files_log_level(flags)); |
830 | | |
831 | 0 | return conf_files_list_strv_at(ret, suffix, rfd, flags, (const char**) d); |
832 | 0 | } |
833 | | |
834 | 0 | int conf_files_list_nulstr_at_full(const char *suffix, int rfd, ConfFilesFlags flags, const char *dirs, ConfFile ***ret_files, size_t *ret_n_files) { |
835 | 0 | _cleanup_strv_free_ char **d = NULL; |
836 | |
|
837 | 0 | assert(ret_files); |
838 | 0 | assert(ret_n_files); |
839 | |
|
840 | 0 | d = strv_split_nulstr(dirs); |
841 | 0 | if (!d) |
842 | 0 | return log_oom_full(conf_files_log_level(flags)); |
843 | | |
844 | 0 | return conf_files_list_strv_at_full(suffix, rfd, flags, (const char**) d, ret_files, ret_n_files); |
845 | 0 | } |
846 | | |
847 | | int conf_files_list_with_replacement( |
848 | | const char *root, |
849 | | char **config_dirs, |
850 | | const char *replacement, |
851 | | char ***ret_files, |
852 | 0 | char **ret_inserted) { |
853 | |
|
854 | 0 | _cleanup_hashmap_free_ Hashmap *fh = NULL; |
855 | 0 | _cleanup_free_ char *inserted = NULL; |
856 | 0 | ConfFilesFlags flags = CONF_FILES_REGULAR | CONF_FILES_FILTER_MASKED_BY_SYMLINK | CONF_FILES_WARN; |
857 | 0 | _cleanup_close_ int rfd = -EBADF; |
858 | 0 | _cleanup_free_ char *root_abs = NULL; |
859 | 0 | _cleanup_strv_free_ char **dirs_abs = NULL; |
860 | 0 | const ConfFile *c = NULL; |
861 | 0 | int r; |
862 | |
|
863 | 0 | assert(ret_files); |
864 | |
|
865 | 0 | r = prepare_dirs(root, flags, config_dirs, &root_abs, &rfd, &dirs_abs); |
866 | 0 | if (r < 0) |
867 | 0 | return r; |
868 | | |
869 | 0 | r = conf_files_list_impl(".conf", root_abs, rfd, flags, (const char * const *) dirs_abs, |
870 | 0 | replacement, &fh, ret_inserted ? &c : NULL); |
871 | 0 | if (r < 0) |
872 | 0 | return r; |
873 | | |
874 | 0 | if (c) { |
875 | 0 | r = chaseat_prefix_root(c->result, root_abs, &inserted); |
876 | 0 | if (r < 0) |
877 | 0 | return log_full_errno(conf_files_log_level(flags), r, |
878 | 0 | "Failed to prefix '%s' with root '%s': %m", |
879 | 0 | c->result, empty_to_root(root_abs)); |
880 | 0 | } |
881 | | |
882 | 0 | r = copy_and_sort_files_from_hashmap(fh, ".conf", empty_to_root(root_abs), flags, ret_files); |
883 | 0 | if (r < 0) |
884 | 0 | return r; |
885 | | |
886 | 0 | if (ret_inserted) |
887 | 0 | *ret_inserted = TAKE_PTR(inserted); |
888 | 0 | return 0; |
889 | 0 | } |
890 | | |
891 | | int conf_files_list_dropins( |
892 | | char ***ret, |
893 | | const char *dropin_dirname, |
894 | | const char *root, |
895 | | int root_fd, |
896 | | ConfFilesFlags flags, |
897 | 58.9k | const char * const *dirs) { |
898 | | |
899 | 58.9k | _cleanup_strv_free_ char **dropin_dirs = NULL; |
900 | 58.9k | const char *suffix; |
901 | 58.9k | int r; |
902 | | |
903 | 58.9k | assert(ret); |
904 | 58.9k | assert(dropin_dirname); |
905 | 58.9k | assert(dirs); |
906 | | |
907 | 58.9k | suffix = strjoina("/", dropin_dirname); |
908 | 58.9k | r = strv_extend_strv_concat(&dropin_dirs, dirs, suffix); |
909 | 58.9k | if (r < 0) |
910 | 0 | return log_oom_full(conf_files_log_level(flags)); |
911 | | |
912 | 58.9k | return conf_files_list_strv_at(ret, ".conf", root_fd, flags, (const char* const*) dropin_dirs); |
913 | 58.9k | } |
914 | | |
915 | | /** |
916 | | * Open and read a config file. |
917 | | * |
918 | | * The <fn> argument may be: |
919 | | * - '-', meaning stdin. |
920 | | * - a file name without a path. In this case <config_dirs> are searched. |
921 | | * - a path, either relative or absolute. In this case <fn> is opened directly. |
922 | | * |
923 | | * This method is only suitable for configuration files which have a flat layout without dropins. |
924 | | */ |
925 | | int conf_file_read( |
926 | | const char *root, |
927 | | const char **config_dirs, |
928 | | const char *fn, |
929 | | parse_line_t parse_line, |
930 | | void *userdata, |
931 | | bool ignore_enoent, |
932 | 0 | bool *invalid_config) { |
933 | |
|
934 | 0 | _cleanup_fclose_ FILE *_f = NULL; |
935 | 0 | _cleanup_free_ char *_fn = NULL; |
936 | 0 | unsigned v = 0; |
937 | 0 | FILE *f; |
938 | 0 | int r = 0; |
939 | |
|
940 | 0 | assert(fn); |
941 | |
|
942 | 0 | if (streq(fn, "-")) { |
943 | 0 | f = stdin; |
944 | 0 | fn = "<stdin>"; |
945 | |
|
946 | 0 | log_debug("Reading config from stdin%s", glyph(GLYPH_ELLIPSIS)); |
947 | |
|
948 | 0 | } else if (is_path(fn)) { |
949 | 0 | r = path_make_absolute_cwd(fn, &_fn); |
950 | 0 | if (r < 0) |
951 | 0 | return log_error_errno(r, "Failed to make path absolute: %m"); |
952 | 0 | fn = _fn; |
953 | |
|
954 | 0 | f = _f = fopen(fn, "re"); |
955 | 0 | if (!_f) |
956 | 0 | r = -errno; |
957 | 0 | else |
958 | 0 | log_debug("Reading config file \"%s\"%s", fn, glyph(GLYPH_ELLIPSIS)); |
959 | |
|
960 | 0 | } else { |
961 | 0 | r = search_and_fopen(fn, "re", root, config_dirs, &_f, &_fn); |
962 | 0 | if (r >= 0) { |
963 | 0 | f = _f; |
964 | 0 | fn = _fn; |
965 | 0 | log_debug("Reading config file \"%s\"%s", fn, glyph(GLYPH_ELLIPSIS)); |
966 | 0 | } |
967 | 0 | } |
968 | | |
969 | 0 | if (r == -ENOENT && ignore_enoent) { |
970 | 0 | log_debug_errno(r, "Failed to open \"%s\", ignoring: %m", fn); |
971 | 0 | return 0; /* No error, but nothing happened. */ |
972 | 0 | } |
973 | 0 | if (r < 0) |
974 | 0 | return log_error_errno(r, "Failed to read '%s': %m", fn); |
975 | | |
976 | 0 | r = 1; /* We entered the part where we may modify state. */ |
977 | |
|
978 | 0 | for (;;) { |
979 | 0 | _cleanup_free_ char *line = NULL; |
980 | 0 | bool invalid_line = false; |
981 | 0 | int k; |
982 | |
|
983 | 0 | k = read_stripped_line(f, LONG_LINE_MAX, &line); |
984 | 0 | if (k < 0) |
985 | 0 | return log_error_errno(k, "Failed to read '%s': %m", fn); |
986 | 0 | if (k == 0) |
987 | 0 | break; |
988 | | |
989 | 0 | v++; |
990 | |
|
991 | 0 | if (IN_SET(line[0], 0, '#')) |
992 | 0 | continue; |
993 | | |
994 | 0 | k = parse_line(fn, v, line, invalid_config ? &invalid_line : NULL, userdata); |
995 | 0 | if (k < 0 && invalid_line) |
996 | | /* Allow reporting with a special code if the caller requested this. */ |
997 | 0 | *invalid_config = true; |
998 | 0 | else |
999 | | /* The first error, if any, becomes our return value. */ |
1000 | 0 | RET_GATHER(r, k); |
1001 | 0 | } |
1002 | | |
1003 | 0 | if (ferror(f)) |
1004 | 0 | RET_GATHER(r, log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to read from file %s.", fn)); |
1005 | |
|
1006 | 0 | return r; |
1007 | 0 | } |