]>
Commit | Line | Data |
---|---|---|
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 |
23 | typedef 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 |
32 | void 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 | ||
59 | bool 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 |
70 | int 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 | 116 | static 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 | 197 | static 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 | 242 | static 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 |
311 | static 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 | 327 | static 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 |
401 | static 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 | ||
422 | static 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 | 462 | int 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 | } |