1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
8 #include "alloc-util.h"
10 #include "edit-util.h"
11 #include "errno-util.h"
16 #include "mkdir-label.h"
17 #include "path-util.h"
18 #include "process-util.h"
19 #include "string-util.h"
21 #include "tmpfile-util-label.h"
23 typedef struct EditFile
{
24 EditFileContext
*context
;
32 void edit_file_context_done(EditFileContext
*context
) {
37 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
38 unlink_and_free(i
->temp
);
40 if (context
->remove_parent
) {
41 _cleanup_free_
char *parent
= NULL
;
43 r
= path_extract_directory(i
->path
, &parent
);
45 log_debug_errno(r
, "Failed to extract directory from '%s', ignoring: %m", i
->path
);
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
);
51 free(i
->original_path
);
52 strv_free(i
->comment_paths
);
55 context
->files
= mfree(context
->files
);
59 bool edit_files_contains(const EditFileContext
*context
, const char *path
) {
63 FOREACH_ARRAY(i
, context
->files
, context
->n_files
)
64 if (path_equal(i
->path
, path
))
71 EditFileContext
*context
,
73 const char *original_path
,
74 char * const *comment_paths
) {
76 _cleanup_free_
char *new_path
= NULL
, *new_original_path
= NULL
;
77 _cleanup_strv_free_
char **new_comment_paths
= NULL
;
82 if (edit_files_contains(context
, path
))
85 if (!GREEDY_REALLOC(context
->files
, context
->n_files
+ 1))
88 new_path
= strdup(path
);
93 new_original_path
= strdup(original_path
);
94 if (!new_original_path
)
99 new_comment_paths
= strv_copy(comment_paths
);
100 if (!new_comment_paths
)
104 context
->files
[context
->n_files
] = (EditFile
) {
106 .path
= TAKE_PTR(new_path
),
107 .original_path
= TAKE_PTR(new_original_path
),
108 .comment_paths
= TAKE_PTR(new_comment_paths
),
116 static int populate_edit_temp_file(EditFile
*e
, FILE *f
, const char *filename
) {
119 assert(!e
->context
->read_from_stdin
);
124 bool has_original
= e
->original_path
&& access(e
->original_path
, F_OK
) >= 0;
125 bool has_target
= access(e
->path
, F_OK
) >= 0;
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. */
131 source
= e
->original_path
;
133 /* Target exists and shouldn't be overwritten. */
138 if (e
->comment_paths
) {
139 _cleanup_free_
char *source_contents
= NULL
;
142 r
= read_full_file(source
, &source_contents
, NULL
);
144 return log_error_errno(r
, "Failed to read source file '%s': %m", source
);
155 e
->context
->marker_start
,
156 strempty(source_contents
),
157 source_contents
&& endswith(source_contents
, "\n") ? "" : "\n",
158 e
->context
->marker_end
);
160 e
->line
= 4; /* Start editing at the contents area */
162 STRV_FOREACH(path
, e
->comment_paths
) {
163 _cleanup_free_
char *comment
= NULL
;
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
))
169 r
= read_full_file(*path
, &comment
, NULL
);
171 return log_error_errno(r
, "Failed to read comment file '%s': %m", *path
);
173 fprintf(f
, "\n\n### %s", *path
);
175 if (!isempty(comment
)) {
176 _cleanup_free_
char *c
= NULL
;
178 c
= strreplace(strstrip(comment
), "\n", "\n# ");
182 fprintf(f
, "\n# %s", c
);
186 r
= copy_file_fd(source
, fileno(f
), COPY_REFLINK
);
188 assert(r
!= -ENOENT
);
189 return log_error_errno(r
, "Failed to copy file '%s' to temporary file '%s': %m",
197 static int create_edit_temp_file(EditFile
*e
, const char *contents
, size_t contents_size
) {
198 _cleanup_(unlink_and_freep
) char *temp
= NULL
;
199 _cleanup_fclose_
FILE *f
= NULL
;
205 assert(!e
->comment_paths
|| (e
->context
->marker_start
&& e
->context
->marker_end
));
206 assert(contents
|| contents_size
== 0);
207 assert(e
->context
->read_from_stdin
== !!contents
);
212 r
= mkdir_parents_label(e
->path
, 0755);
214 return log_error_errno(r
, "Failed to create parent directories for '%s': %m", e
->path
);
216 r
= fopen_temporary_label(e
->path
, e
->path
, &f
, &temp
);
218 return log_error_errno(r
, "Failed to create temporary file for '%s': %m", e
->path
);
220 if (fchmod(fileno(f
), 0644) < 0)
221 return log_error_errno(errno
, "Failed to change mode of temporary file '%s': %m", temp
);
223 if (e
->context
->read_from_stdin
) {
224 if (fwrite(contents
, 1, contents_size
, f
) != contents_size
)
225 return log_error_errno(SYNTHETIC_ERRNO(EIO
),
226 "Failed to write stdin data to temporary file '%s'.", temp
);
228 r
= populate_edit_temp_file(e
, f
, temp
);
233 r
= fflush_and_check(f
);
235 return log_error_errno(r
, "Failed to write to temporary file '%s': %m", temp
);
237 e
->temp
= TAKE_PTR(temp
);
242 static int run_editor_child(const EditFileContext
*context
) {
243 _cleanup_strv_free_
char **args
= NULL
, **editor
= NULL
;
247 assert(context
->n_files
>= 1);
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. */
252 FOREACH_STRING(e
, "SYSTEMD_EDITOR", "EDITOR", "VISUAL") {
253 const char *m
= empty_to_null(getenv(e
));
255 editor
= strv_split(m
, WHITESPACE
);
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
);
270 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
271 r
= strv_extend(&args
, i
->temp
);
276 size_t editor_n
= strv_length(editor
);
278 /* Strings are owned by 'editor' and 'args' */
279 _cleanup_free_
char **cmdline
= new(char*, editor_n
+ strv_length(args
) + 1);
283 *mempcpy_typesafe(mempcpy_typesafe(cmdline
, editor
, editor_n
), args
, strv_length(args
)) = NULL
;
285 execvp(cmdline
[0], cmdline
);
286 log_warning_errno(errno
, "Specified editor '%s' not available, trying fallbacks: %m", editor
[0]);
289 bool prepended
= false;
290 FOREACH_STRING(name
, "editor", "nano", "vim", "vi") {
292 r
= strv_prepend(&args
, name
);
295 r
= free_and_strdup(&args
[0], name
);
299 execvp(args
[0], (char* const*) args
);
301 /* We do not fail if the editor doesn't exist because we want to try each one of them
304 return log_error_errno(errno
, "Failed to execute '%s': %m", name
);
307 return log_error_errno(SYNTHETIC_ERRNO(ENOENT
),
308 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
311 static int run_editor(const EditFileContext
*context
) {
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
);
319 if (r
== 0) { /* Child */
320 r
= run_editor_child(context
);
321 _exit(r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
);
327 static int strip_edit_temp_file(EditFile
*e
) {
328 _cleanup_free_
char *old_contents
= NULL
, *tmp
= NULL
, *new_contents
= NULL
;
329 const char *stripped
;
335 assert(!e
->context
->marker_start
== !e
->context
->marker_end
);
338 r
= read_full_file(e
->temp
, &old_contents
, NULL
);
340 return log_error_errno(r
, "Failed to read temporary file '%s': %m", e
->temp
);
342 tmp
= strdup(old_contents
);
346 with_marker
= e
->context
->marker_start
&& !e
->context
->read_from_stdin
;
349 /* Trim out the lines between the two markers */
350 char *contents_start
, *contents_end
;
352 contents_start
= strstrafter(tmp
, e
->context
->marker_start
) ?: tmp
;
354 contents_end
= strstr(contents_start
, e
->context
->marker_end
);
356 *contents_end
= '\0';
358 stripped
= strstrip(contents_start
);
360 stripped
= strstrip(tmp
);
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. */
366 for (const char *p
= old_contents
;;) {
367 p
= skip_leading_chars(p
, WHITESPACE
);
371 log_warning("Found modifications outside of the staging area, which would be discarded.");
375 /* Skip the whole line if commented out */
382 return 0; /* File is empty (has no real changes) */
385 /* Trim prefix and suffix, but ensure suffixed by single newline */
386 new_contents
= strjoin(stripped
, "\n");
390 if (streq(old_contents
, new_contents
)) /* Don't touch the file if the above didn't change a thing */
391 return 1; /* Contents have real changes */
393 r
= write_string_file(e
->temp
, new_contents
,
394 WRITE_STRING_FILE_TRUNCATE
| WRITE_STRING_FILE_AVOID_NEWLINE
);
396 return log_error_errno(r
, "Failed to strip temporary file '%s': %m", e
->temp
);
398 return 1; /* Contents have real changes */
401 static int edit_file_install_one(EditFile
*e
) {
408 r
= strip_edit_temp_file(e
);
412 r
= RET_NERRNO(rename(e
->temp
, e
->path
));
414 return log_error_errno(r
,
415 "Failed to rename temporary file '%s' to target file '%s': %m",
417 e
->temp
= mfree(e
->temp
);
422 static int edit_file_install_one_stdin(EditFile
*e
, const char *contents
, size_t contents_size
, int *fd
) {
427 assert(contents
|| contents_size
== 0);
430 if (contents_size
== 0)
434 r
= mkdir_parents_label(e
->path
, 0755);
436 return log_error_errno(r
, "Failed to create parent directories for '%s': %m", e
->path
);
438 r
= copy_file_atomic_at(*fd
, NULL
, AT_FDCWD
, e
->path
, 0644, COPY_REFLINK
|COPY_REPLACE
|COPY_MAC_CREATE
);
440 return log_error_errno(r
, "Failed to copy stdin contents to '%s': %m", e
->path
);
445 r
= create_edit_temp_file(e
, contents
, contents_size
);
449 _cleanup_close_
int tfd
= open(e
->temp
, O_PATH
|O_CLOEXEC
);
451 return log_error_errno(errno
, "Failed to pin temporary file '%s': %m", e
->temp
);
453 r
= edit_file_install_one(e
);
462 int do_edit_files_and_install(EditFileContext
*context
) {
463 _cleanup_free_
char *stdin_data
= NULL
;
464 size_t stdin_size
= 0;
469 if (context
->n_files
== 0)
470 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT
), "Got no files to edit.");
472 if (context
->read_from_stdin
) {
473 r
= read_full_stream(stdin
, &stdin_data
, &stdin_size
);
475 return log_error_errno(r
, "Failed to read stdin: %m");
477 FOREACH_ARRAY(editfile
, context
->files
, context
->n_files
) {
478 r
= create_edit_temp_file(editfile
, /* contents = */ NULL
, /* contents_size = */ 0);
483 r
= run_editor(context
);
488 _cleanup_close_
int stdin_data_fd
= -EBADF
;
490 FOREACH_ARRAY(editfile
, context
->files
, context
->n_files
) {
491 if (context
->read_from_stdin
) {
492 r
= edit_file_install_one_stdin(editfile
, stdin_data
, stdin_size
, &stdin_data_fd
);
494 log_notice("Stripped stdin content is empty, not writing file.");
498 r
= edit_file_install_one(editfile
);
500 log_notice("%s: after editing, new contents are empty, not writing file.",
508 log_info("Successfully installed edited file '%s'.", editfile
->path
);