1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
6 #include "alloc-util.h"
12 #include "mkdir-label.h"
13 #include "path-util.h"
14 #include "process-util.h"
15 #include "string-util.h"
17 #include "tmpfile-util-label.h"
19 typedef struct EditFile
{
20 EditFileContext
*context
;
28 void edit_file_context_done(EditFileContext
*context
) {
33 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
34 unlink_and_free(i
->temp
);
36 if (context
->remove_parent
) {
37 _cleanup_free_
char *parent
= NULL
;
39 r
= path_extract_directory(i
->path
, &parent
);
41 log_debug_errno(r
, "Failed to extract directory from '%s', ignoring: %m", i
->path
);
42 else if (rmdir(parent
) < 0 && !IN_SET(errno
, ENOENT
, ENOTEMPTY
))
43 log_debug_errno(errno
, "Failed to remove parent directory '%s', ignoring: %m", parent
);
47 free(i
->original_path
);
48 strv_free(i
->comment_paths
);
51 context
->files
= mfree(context
->files
);
55 bool edit_files_contains(const EditFileContext
*context
, const char *path
) {
59 FOREACH_ARRAY(i
, context
->files
, context
->n_files
)
60 if (path_equal(i
->path
, path
))
67 EditFileContext
*context
,
69 const char *original_path
,
70 char * const *comment_paths
) {
72 _cleanup_free_
char *new_path
= NULL
, *new_original_path
= NULL
;
73 _cleanup_strv_free_
char **new_comment_paths
= NULL
;
78 if (edit_files_contains(context
, path
))
81 if (!GREEDY_REALLOC(context
->files
, context
->n_files
+ 1))
84 new_path
= strdup(path
);
89 new_original_path
= strdup(original_path
);
90 if (!new_original_path
)
95 new_comment_paths
= strv_copy(comment_paths
);
96 if (!new_comment_paths
)
100 context
->files
[context
->n_files
] = (EditFile
) {
102 .path
= TAKE_PTR(new_path
),
103 .original_path
= TAKE_PTR(new_original_path
),
104 .comment_paths
= TAKE_PTR(new_comment_paths
),
112 static int populate_edit_temp_file(EditFile
*e
, FILE *f
, const char *filename
) {
117 bool has_original
= e
->original_path
&& access(e
->original_path
, F_OK
) >= 0;
118 bool has_target
= access(e
->path
, F_OK
) >= 0;
122 if (has_original
&& (!has_target
|| e
->context
->overwrite_with_origin
))
123 /* We are asked to overwrite target with original_path or target doesn't exist. */
124 source
= e
->original_path
;
126 /* Target exists and shouldn't be overwritten. */
131 if (e
->comment_paths
) {
132 _cleanup_free_
char *source_contents
= NULL
;
135 r
= read_full_file(source
, &source_contents
, NULL
);
137 return log_error_errno(r
, "Failed to read source file '%s': %m", source
);
148 e
->context
->marker_start
,
149 strempty(source_contents
),
150 source_contents
&& endswith(source_contents
, "\n") ? "" : "\n",
151 e
->context
->marker_end
);
153 e
->line
= 4; /* Start editing at the contents area */
155 STRV_FOREACH(path
, e
->comment_paths
) {
156 _cleanup_free_
char *comment
= NULL
;
158 /* Skip the file which is being edited and the source file (can be the same) */
159 if (PATH_IN_SET(*path
, e
->path
, source
))
162 r
= read_full_file(*path
, &comment
, NULL
);
164 return log_error_errno(r
, "Failed to read comment file '%s': %m", *path
);
166 fprintf(f
, "\n\n### %s", *path
);
168 if (!isempty(comment
)) {
169 _cleanup_free_
char *c
= NULL
;
171 c
= strreplace(strstrip(comment
), "\n", "\n# ");
175 fprintf(f
, "\n# %s", c
);
179 r
= copy_file_fd(source
, fileno(f
), COPY_REFLINK
);
181 assert(r
!= -ENOENT
);
182 return log_error_errno(r
, "Failed to copy file '%s' to temporary file '%s': %m",
190 static int create_edit_temp_file(EditFile
*e
, const char *contents
, size_t contents_size
) {
191 _cleanup_(unlink_and_freep
) char *temp
= NULL
;
192 _cleanup_fclose_
FILE *f
= NULL
;
198 assert(!e
->comment_paths
|| (e
->context
->marker_start
&& e
->context
->marker_end
));
199 assert(contents
|| contents_size
== 0);
204 r
= mkdir_parents_label(e
->path
, 0755);
206 return log_error_errno(r
, "Failed to create parent directories for '%s': %m", e
->path
);
208 r
= fopen_temporary_label(e
->path
, e
->path
, &f
, &temp
);
210 return log_error_errno(r
, "Failed to create temporary file for '%s': %m", e
->path
);
212 if (fchmod(fileno(f
), 0644) < 0)
213 return log_error_errno(errno
, "Failed to change mode of temporary file '%s': %m", temp
);
215 if (e
->context
->stdin
) {
216 if (fwrite(contents
, 1, contents_size
, f
) != contents_size
)
217 return log_error_errno(SYNTHETIC_ERRNO(EIO
),
218 "Failed to copy input to temporary file '%s'.", temp
);
220 r
= populate_edit_temp_file(e
, f
, temp
);
225 r
= fflush_and_check(f
);
227 return log_error_errno(r
, "Failed to write to temporary file '%s': %m", temp
);
229 e
->temp
= TAKE_PTR(temp
);
234 static int run_editor_child(const EditFileContext
*context
) {
235 _cleanup_strv_free_
char **args
= NULL
;
239 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
240 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
241 * well known editors. */
242 editor
= getenv("SYSTEMD_EDITOR");
244 editor
= getenv("EDITOR");
246 editor
= getenv("VISUAL");
248 if (!isempty(editor
)) {
249 _cleanup_strv_free_
char **editor_args
= NULL
;
251 editor_args
= strv_split(editor
, WHITESPACE
);
255 args
= TAKE_PTR(editor_args
);
258 if (context
->n_files
== 1 && context
->files
[0].line
> 1) {
259 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
260 r
= strv_extendf(&args
, "+%u", context
->files
[0].line
);
265 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
266 r
= strv_extend(&args
, i
->temp
);
271 if (!isempty(editor
))
272 execvp(args
[0], (char* const*) args
);
274 bool prepended
= false;
275 FOREACH_STRING(name
, "editor", "nano", "vim", "vi") {
277 r
= strv_prepend(&args
, name
);
280 r
= free_and_strdup(&args
[0], name
);
284 execvp(args
[0], (char* const*) args
);
286 /* We do not fail if the editor doesn't exist because we want to try each one of them
289 return log_error_errno(errno
, "Failed to execute '%s': %m", name
);
292 return log_error_errno(SYNTHETIC_ERRNO(ENOENT
),
293 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
296 static int run_editor(const EditFileContext
*context
) {
301 r
= safe_fork("(editor)", FORK_RESET_SIGNALS
|FORK_DEATHSIG_SIGTERM
|FORK_RLIMIT_NOFILE_SAFE
|FORK_LOG
|FORK_WAIT
, NULL
);
304 if (r
== 0) { /* Child */
305 r
= run_editor_child(context
);
306 _exit(r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
);
312 static int strip_edit_temp_file(EditFile
*e
) {
313 _cleanup_free_
char *old_contents
= NULL
, *tmp
= NULL
, *new_contents
= NULL
;
314 const char *stripped
;
321 r
= read_full_file(e
->temp
, &old_contents
, NULL
);
323 return log_error_errno(r
, "Failed to read temporary file '%s': %m", e
->temp
);
325 tmp
= strdup(old_contents
);
329 if (e
->context
->marker_start
&& !e
->context
->stdin
) {
330 /* Trim out the lines between the two markers */
331 char *contents_start
, *contents_end
;
333 assert(e
->context
->marker_end
);
335 contents_start
= strstrafter(tmp
, e
->context
->marker_start
) ?: tmp
;
337 contents_end
= strstr(contents_start
, e
->context
->marker_end
);
339 *contents_end
= '\0';
341 stripped
= strstrip(contents_start
);
343 stripped
= strstrip(tmp
);
345 if (isempty(stripped
)) {
346 /* File is empty (has no real changes) */
347 log_notice("%s: after editing, new contents are empty, not writing file.", e
->path
);
351 /* Trim prefix and suffix, but ensure suffixed by single newline */
352 new_contents
= strjoin(stripped
, "\n");
356 if (streq(old_contents
, new_contents
)) /* Don't touch the file if the above didn't change a thing */
357 return 1; /* Contents have real changes */
359 r
= write_string_file(e
->temp
, new_contents
,
360 WRITE_STRING_FILE_CREATE
| WRITE_STRING_FILE_TRUNCATE
| WRITE_STRING_FILE_AVOID_NEWLINE
);
362 return log_error_errno(r
, "Failed to strip temporary file '%s': %m", e
->temp
);
364 return 1; /* Contents have real changes */
367 int do_edit_files_and_install(EditFileContext
*context
) {
368 _cleanup_free_
char *data
= NULL
;
369 size_t data_size
= 0;
374 if (context
->n_files
== 0)
375 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT
), "Got no files to edit.");
377 if (context
->stdin
) {
378 r
= read_full_stream(stdin
, &data
, &data_size
);
380 return log_error_errno(r
, "Failed to read stdin: %m");
383 FOREACH_ARRAY(editfile
, context
->files
, context
->n_files
) {
384 r
= create_edit_temp_file(editfile
, data
, data_size
);
389 if (!context
->stdin
) {
390 r
= run_editor(context
);
395 FOREACH_ARRAY(editfile
, context
->files
, context
->n_files
) {
396 /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */
397 r
= strip_edit_temp_file(editfile
);
400 if (r
== 0) /* temp file doesn't carry actual changes, ignoring */
403 r
= RET_NERRNO(rename(editfile
->temp
, editfile
->path
));
405 return log_error_errno(r
,
406 "Failed to rename temporary file '%s' to target file '%s': %m",
409 editfile
->temp
= mfree(editfile
->temp
);
411 log_info("Successfully installed edited file '%s'.", editfile
->path
);