]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/edit-util.c
man/systemd-sysext: list ephemeral/ephemeral-import in the list of options
[thirdparty/systemd.git] / src / shared / edit-util.c
CommitLineData
a01c4bc9
MY
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
a01c4bc9 3#include <stdio.h>
69a283c5
DDM
4#include <stdlib.h>
5#include <sys/stat.h>
4f18ff2e 6#include <unistd.h>
a01c4bc9
MY
7
8#include "alloc-util.h"
9#include "copy.h"
10#include "edit-util.h"
fda65211 11#include "errno-util.h"
a01c4bc9
MY
12#include "fd-util.h"
13#include "fileio.h"
14#include "fs-util.h"
93a1f792 15#include "log.h"
a01c4bc9
MY
16#include "mkdir-label.h"
17#include "path-util.h"
18#include "process-util.h"
a01c4bc9
MY
19#include "string-util.h"
20#include "strv.h"
54ad6aa1 21#include "tmpfile-util-label.h"
a01c4bc9 22
0e363838
MY
23typedef struct EditFile {
24 EditFileContext *context;
25 char *path;
26 char *original_path;
27 char **comment_paths;
28 char *temp;
29 unsigned line;
30} EditFile;
31
9a11b4f9
MY
32void edit_file_context_done(EditFileContext *context) {
33 int r;
34
35 assert(context);
36
37 FOREACH_ARRAY(i, context->files, context->n_files) {
8992667f 38 unlink_and_free(i->temp);
9a11b4f9
MY
39
40 if (context->remove_parent) {
41 _cleanup_free_ char *parent = NULL;
42
43 r = path_extract_directory(i->path, &parent);
44 if (r < 0)
45 log_debug_errno(r, "Failed to extract directory from '%s', ignoring: %m", i->path);
d3bf024f
MY
46 else if (rmdir(parent) < 0 && !IN_SET(errno, ENOENT, ENOTEMPTY))
47 log_debug_errno(errno, "Failed to remove parent directory '%s', ignoring: %m", parent);
9a11b4f9 48 }
a01c4bc9 49
a01c4bc9 50 free(i->path);
9a11b4f9
MY
51 free(i->original_path);
52 strv_free(i->comment_paths);
a01c4bc9
MY
53 }
54
9a11b4f9
MY
55 context->files = mfree(context->files);
56 context->n_files = 0;
57}
58
59bool edit_files_contains(const EditFileContext *context, const char *path) {
60 assert(context);
61 assert(path);
62
63 FOREACH_ARRAY(i, context->files, context->n_files)
00b6baf2 64 if (path_equal(i->path, path))
9a11b4f9
MY
65 return true;
66
67 return false;
a01c4bc9
MY
68}
69
9a11b4f9
MY
70int edit_files_add(
71 EditFileContext *context,
72 const char *path,
73 const char *original_path,
74 char * const *comment_paths) {
75
76 _cleanup_free_ char *new_path = NULL, *new_original_path = NULL;
77 _cleanup_strv_free_ char **new_comment_paths = NULL;
78
79 assert(context);
80 assert(path);
81
82 if (edit_files_contains(context, path))
83 return 0;
84
da037170 85 if (!GREEDY_REALLOC(context->files, context->n_files + 1))
9a11b4f9
MY
86 return log_oom();
87
88 new_path = strdup(path);
89 if (!new_path)
90 return log_oom();
91
92 if (original_path) {
93 new_original_path = strdup(original_path);
94 if (!new_original_path)
95 return log_oom();
96 }
97
98 if (comment_paths) {
99 new_comment_paths = strv_copy(comment_paths);
100 if (!new_comment_paths)
101 return log_oom();
102 }
103
104 context->files[context->n_files] = (EditFile) {
043db340 105 .context = context,
9a11b4f9
MY
106 .path = TAKE_PTR(new_path),
107 .original_path = TAKE_PTR(new_original_path),
108 .comment_paths = TAKE_PTR(new_comment_paths),
5161836b 109 .line = 1,
9a11b4f9
MY
110 };
111 context->n_files++;
112
113 return 1;
114}
115
5161836b 116static int populate_edit_temp_file(EditFile *e, FILE *f, const char *filename) {
c629a5bc 117 assert(e);
40f5c372 118 assert(e->context);
79f0e94e 119 assert(!e->context->read_from_stdin);
40f5c372 120 assert(e->path);
5161836b
ZJS
121 assert(f);
122 assert(filename);
c629a5bc 123
5161836b
ZJS
124 bool has_original = e->original_path && access(e->original_path, F_OK) >= 0;
125 bool has_target = access(e->path, F_OK) >= 0;
126 const char *source;
127 int r;
52073ba2 128
bc6c7a58
MY
129 if (has_original && (!has_target || e->context->overwrite_with_origin))
130 /* We are asked to overwrite target with original_path or target doesn't exist. */
54ad6aa1
MY
131 source = e->original_path;
132 else if (has_target)
bc6c7a58 133 /* Target exists and shouldn't be overwritten. */
54ad6aa1
MY
134 source = e->path;
135 else
136 source = NULL;
fa2413dd 137
c629a5bc 138 if (e->comment_paths) {
54ad6aa1 139 _cleanup_free_ char *source_contents = NULL;
a01c4bc9 140
54ad6aa1
MY
141 if (source) {
142 r = read_full_file(source, &source_contents, NULL);
143 if (r < 0)
144 return log_error_errno(r, "Failed to read source file '%s': %m", source);
145 }
a01c4bc9
MY
146
147 fprintf(f,
148 "### Editing %s\n"
149 "%s\n"
150 "\n"
151 "%s%s"
152 "\n"
153 "%s\n",
c629a5bc
YW
154 e->path,
155 e->context->marker_start,
54ad6aa1
MY
156 strempty(source_contents),
157 source_contents && endswith(source_contents, "\n") ? "" : "\n",
c629a5bc 158 e->context->marker_end);
a01c4bc9 159
5161836b 160 e->line = 4; /* Start editing at the contents area */
a01c4bc9 161
c629a5bc 162 STRV_FOREACH(path, e->comment_paths) {
54ad6aa1 163 _cleanup_free_ char *comment = NULL;
a01c4bc9 164
54ad6aa1
MY
165 /* Skip the file which is being edited and the source file (can be the same) */
166 if (PATH_IN_SET(*path, e->path, source))
a01c4bc9
MY
167 continue;
168
54ad6aa1 169 r = read_full_file(*path, &comment, NULL);
a01c4bc9 170 if (r < 0)
54ad6aa1 171 return log_error_errno(r, "Failed to read comment file '%s': %m", *path);
a01c4bc9
MY
172
173 fprintf(f, "\n\n### %s", *path);
a01c4bc9 174
54ad6aa1
MY
175 if (!isempty(comment)) {
176 _cleanup_free_ char *c = NULL;
177
178 c = strreplace(strstrip(comment), "\n", "\n# ");
179 if (!c)
a01c4bc9 180 return log_oom();
fa2413dd 181
54ad6aa1 182 fprintf(f, "\n# %s", c);
a01c4bc9
MY
183 }
184 }
54ad6aa1
MY
185 } else if (source) {
186 r = copy_file_fd(source, fileno(f), COPY_REFLINK);
187 if (r < 0) {
188 assert(r != -ENOENT);
5161836b
ZJS
189 return log_error_errno(r, "Failed to copy file '%s' to temporary file '%s': %m",
190 source, filename);
54ad6aa1 191 }
a01c4bc9
MY
192 }
193
5161836b
ZJS
194 return 0;
195}
196
329050c5 197static int create_edit_temp_file(EditFile *e, const char *contents, size_t contents_size) {
5161836b
ZJS
198 _cleanup_(unlink_and_freep) char *temp = NULL;
199 _cleanup_fclose_ FILE *f = NULL;
200 int r;
201
202 assert(e);
203 assert(e->context);
204 assert(e->path);
205 assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end));
329050c5 206 assert(contents || contents_size == 0);
79f0e94e 207 assert(e->context->read_from_stdin == !!contents);
5161836b
ZJS
208
209 if (e->temp)
210 return 0;
211
212 r = mkdir_parents_label(e->path, 0755);
213 if (r < 0)
214 return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path);
215
216 r = fopen_temporary_label(e->path, e->path, &f, &temp);
217 if (r < 0)
218 return log_error_errno(r, "Failed to create temporary file for '%s': %m", e->path);
219
220 if (fchmod(fileno(f), 0644) < 0)
221 return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp);
222
79f0e94e 223 if (e->context->read_from_stdin) {
329050c5
ZJS
224 if (fwrite(contents, 1, contents_size, f) != contents_size)
225 return log_error_errno(SYNTHETIC_ERRNO(EIO),
40f5c372 226 "Failed to write stdin data to temporary file '%s'.", temp);
329050c5
ZJS
227 } else {
228 r = populate_edit_temp_file(e, f, temp);
229 if (r < 0)
230 return r;
231 }
5161836b 232
54ad6aa1
MY
233 r = fflush_and_check(f);
234 if (r < 0)
235 return log_error_errno(r, "Failed to write to temporary file '%s': %m", temp);
236
c629a5bc 237 e->temp = TAKE_PTR(temp);
a01c4bc9
MY
238
239 return 0;
240}
241
f8970df5 242static int run_editor_child(const EditFileContext *context) {
d2e49d93 243 _cleanup_strv_free_ char **args = NULL, **editor = NULL;
a01c4bc9
MY
244 int r;
245
7a729f87
MY
246 assert(context);
247 assert(context->n_files >= 1);
248
f8970df5
MY
249 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
250 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
251 * well known editors. */
7a729f87 252 FOREACH_STRING(e, "SYSTEMD_EDITOR", "EDITOR", "VISUAL") {
d2e49d93
MY
253 const char *m = empty_to_null(getenv(e));
254 if (m) {
255 editor = strv_split(m, WHITESPACE);
256 if (!editor)
257 return log_oom();
a01c4bc9 258
d2e49d93
MY
259 break;
260 }
f8970df5 261 }
a01c4bc9 262
f8970df5
MY
263 if (context->n_files == 1 && context->files[0].line > 1) {
264 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
265 r = strv_extendf(&args, "+%u", context->files[0].line);
266 if (r < 0)
267 return log_oom();
268 }
a01c4bc9 269
f8970df5
MY
270 FOREACH_ARRAY(i, context->files, context->n_files) {
271 r = strv_extend(&args, i->temp);
272 if (r < 0)
273 return log_oom();
274 }
a01c4bc9 275
d2e49d93
MY
276 size_t editor_n = strv_length(editor);
277 if (editor_n > 0) {
278 /* Strings are owned by 'editor' and 'args' */
279 _cleanup_free_ char **cmdline = new(char*, editor_n + strv_length(args) + 1);
280 if (!cmdline)
281 return log_oom();
282
283 *mempcpy_typesafe(mempcpy_typesafe(cmdline, editor, editor_n), args, strv_length(args)) = NULL;
284
285 execvp(cmdline[0], cmdline);
286 log_warning_errno(errno, "Specified editor '%s' not available, trying fallbacks: %m", editor[0]);
287 }
f8970df5
MY
288
289 bool prepended = false;
290 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
291 if (!prepended) {
292 r = strv_prepend(&args, name);
293 prepended = true;
a01c4bc9 294 } else
f8970df5
MY
295 r = free_and_strdup(&args[0], name);
296 if (r < 0)
297 return log_oom();
298
299 execvp(args[0], (char* const*) args);
300
301 /* We do not fail if the editor doesn't exist because we want to try each one of them
302 * before failing. */
303 if (errno != ENOENT)
304 return log_error_errno(errno, "Failed to execute '%s': %m", name);
305 }
306
307 return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
308 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
309}
a01c4bc9 310
f8970df5
MY
311static int run_editor(const EditFileContext *context) {
312 int r;
313
314 assert(context);
315
7a729f87 316 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG|FORK_LOG|FORK_WAIT, NULL);
f8970df5
MY
317 if (r < 0)
318 return r;
319 if (r == 0) { /* Child */
320 r = run_editor_child(context);
321 _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
a01c4bc9
MY
322 }
323
324 return 0;
325}
326
978e222f 327static int strip_edit_temp_file(EditFile *e) {
232f017b 328 _cleanup_free_ char *old_contents = NULL, *tmp = NULL, *new_contents = NULL;
978e222f 329 const char *stripped;
e65b0729 330 bool with_marker;
a01c4bc9
MY
331 int r;
332
aec8200b
YW
333 assert(e);
334 assert(e->context);
e65b0729 335 assert(!e->context->marker_start == !e->context->marker_end);
aec8200b 336 assert(e->temp);
fa2413dd 337
aec8200b 338 r = read_full_file(e->temp, &old_contents, NULL);
a01c4bc9 339 if (r < 0)
77c9bb17 340 return log_error_errno(r, "Failed to read temporary file '%s': %m", e->temp);
aec8200b 341
232f017b
ZJS
342 tmp = strdup(old_contents);
343 if (!tmp)
344 return log_oom();
345
79f0e94e 346 with_marker = e->context->marker_start && !e->context->read_from_stdin;
e65b0729
MY
347
348 if (with_marker) {
978e222f 349 /* Trim out the lines between the two markers */
aec8200b
YW
350 char *contents_start, *contents_end;
351
232f017b 352 contents_start = strstrafter(tmp, e->context->marker_start) ?: tmp;
a01c4bc9 353
aec8200b
YW
354 contents_end = strstr(contents_start, e->context->marker_end);
355 if (contents_end)
978e222f 356 *contents_end = '\0';
a01c4bc9 357
978e222f 358 stripped = strstrip(contents_start);
aec8200b 359 } else
232f017b
ZJS
360 stripped = strstrip(tmp);
361
e65b0729
MY
362 if (isempty(stripped)) {
363 /* People keep coming back to #24208 due to edits outside of markers. Let's detect this
364 * and point them in the right direction. */
365 if (with_marker)
366 for (const char *p = old_contents;;) {
367 p = skip_leading_chars(p, WHITESPACE);
368 if (*p == '\0')
369 break;
370 if (*p != '#') {
371 log_warning("Found modifications outside of the staging area, which would be discarded.");
372 break;
373 }
374
375 /* Skip the whole line if commented out */
376 p = strchr(p, '\n');
377 if (!p)
378 break;
379 p++;
380 }
381
40f5c372 382 return 0; /* File is empty (has no real changes) */
e65b0729 383 }
a01c4bc9 384
978e222f
MY
385 /* Trim prefix and suffix, but ensure suffixed by single newline */
386 new_contents = strjoin(stripped, "\n");
a01c4bc9
MY
387 if (!new_contents)
388 return log_oom();
389
390 if (streq(old_contents, new_contents)) /* Don't touch the file if the above didn't change a thing */
232f017b 391 return 1; /* Contents have real changes */
a01c4bc9 392
232f017b 393 r = write_string_file(e->temp, new_contents,
3b5b2ff8 394 WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
a01c4bc9 395 if (r < 0)
77c9bb17 396 return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp);
a01c4bc9 397
232f017b 398 return 1; /* Contents have real changes */
a01c4bc9 399}
9a11b4f9 400
40f5c372
MY
401static int edit_file_install_one(EditFile *e) {
402 int r;
403
404 assert(e);
405 assert(e->path);
406 assert(e->temp);
407
408 r = strip_edit_temp_file(e);
409 if (r <= 0)
410 return r;
411
412 r = RET_NERRNO(rename(e->temp, e->path));
413 if (r < 0)
414 return log_error_errno(r,
415 "Failed to rename temporary file '%s' to target file '%s': %m",
416 e->temp, e->path);
417 e->temp = mfree(e->temp);
418
419 return 1;
420}
421
422static int edit_file_install_one_stdin(EditFile *e, const char *contents, size_t contents_size, int *fd) {
423 int r;
424
425 assert(e);
426 assert(e->path);
427 assert(contents || contents_size == 0);
428 assert(fd);
429
430 if (contents_size == 0)
431 return 0;
432
433 if (*fd >= 0) {
434 r = mkdir_parents_label(e->path, 0755);
435 if (r < 0)
436 return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path);
437
438 r = copy_file_atomic_at(*fd, NULL, AT_FDCWD, e->path, 0644, COPY_REFLINK|COPY_REPLACE|COPY_MAC_CREATE);
439 if (r < 0)
440 return log_error_errno(r, "Failed to copy stdin contents to '%s': %m", e->path);
441
442 return 1;
443 }
444
445 r = create_edit_temp_file(e, contents, contents_size);
446 if (r < 0)
447 return r;
448
449 _cleanup_close_ int tfd = open(e->temp, O_PATH|O_CLOEXEC);
450 if (tfd < 0)
451 return log_error_errno(errno, "Failed to pin temporary file '%s': %m", e->temp);
452
453 r = edit_file_install_one(e);
454 if (r <= 0)
455 return r;
456
457 *fd = TAKE_FD(tfd);
458
459 return 1;
460}
461
9a11b4f9 462int do_edit_files_and_install(EditFileContext *context) {
40f5c372
MY
463 _cleanup_free_ char *stdin_data = NULL;
464 size_t stdin_size = 0;
9a11b4f9
MY
465 int r;
466
467 assert(context);
468
469 if (context->n_files == 0)
470 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
471
79f0e94e 472 if (context->read_from_stdin) {
40f5c372 473 r = read_full_stream(stdin, &stdin_data, &stdin_size);
329050c5
ZJS
474 if (r < 0)
475 return log_error_errno(r, "Failed to read stdin: %m");
40f5c372
MY
476 } else {
477 FOREACH_ARRAY(editfile, context->files, context->n_files) {
478 r = create_edit_temp_file(editfile, /* contents = */ NULL, /* contents_size = */ 0);
479 if (r < 0)
480 return r;
481 }
9a11b4f9 482
329050c5
ZJS
483 r = run_editor(context);
484 if (r < 0)
485 return r;
486 }
9a11b4f9 487
40f5c372
MY
488 _cleanup_close_ int stdin_data_fd = -EBADF;
489
329050c5 490 FOREACH_ARRAY(editfile, context->files, context->n_files) {
79f0e94e 491 if (context->read_from_stdin) {
40f5c372
MY
492 r = edit_file_install_one_stdin(editfile, stdin_data, stdin_size, &stdin_data_fd);
493 if (r == 0) {
494 log_notice("Stripped stdin content is empty, not writing file.");
495 return 0;
496 }
497 } else {
498 r = edit_file_install_one(editfile);
499 if (r == 0) {
500 log_notice("%s: after editing, new contents are empty, not writing file.",
501 editfile->path);
502 continue;
503 }
504 }
9a11b4f9
MY
505 if (r < 0)
506 return r;
9a11b4f9 507
329050c5 508 log_info("Successfully installed edited file '%s'.", editfile->path);
9a11b4f9
MY
509 }
510
511 return 0;
512}