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 void edit_file_context_done(EditFileContext
*context
) {
24 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
25 unlink_and_free(i
->temp
);
27 if (context
->remove_parent
) {
28 _cleanup_free_
char *parent
= NULL
;
30 r
= path_extract_directory(i
->path
, &parent
);
32 log_debug_errno(r
, "Failed to extract directory from '%s', ignoring: %m", i
->path
);
33 else if (rmdir(parent
) < 0 && !IN_SET(errno
, ENOENT
, ENOTEMPTY
))
34 log_debug_errno(errno
, "Failed to remove parent directory '%s', ignoring: %m", parent
);
38 free(i
->original_path
);
39 strv_free(i
->comment_paths
);
42 context
->files
= mfree(context
->files
);
46 bool edit_files_contains(const EditFileContext
*context
, const char *path
) {
50 FOREACH_ARRAY(i
, context
->files
, context
->n_files
)
51 if (path_equal(i
->path
, path
))
58 EditFileContext
*context
,
60 const char *original_path
,
61 char * const *comment_paths
) {
63 _cleanup_free_
char *new_path
= NULL
, *new_original_path
= NULL
;
64 _cleanup_strv_free_
char **new_comment_paths
= NULL
;
69 if (edit_files_contains(context
, path
))
72 if (!GREEDY_REALLOC(context
->files
, context
->n_files
+ 1))
75 new_path
= strdup(path
);
80 new_original_path
= strdup(original_path
);
81 if (!new_original_path
)
86 new_comment_paths
= strv_copy(comment_paths
);
87 if (!new_comment_paths
)
91 context
->files
[context
->n_files
] = (EditFile
) {
93 .path
= TAKE_PTR(new_path
),
94 .original_path
= TAKE_PTR(new_original_path
),
95 .comment_paths
= TAKE_PTR(new_comment_paths
),
102 static int create_edit_temp_file(EditFile
*e
) {
103 _cleanup_(unlink_and_freep
) char *temp
= NULL
;
104 _cleanup_fclose_
FILE *f
= NULL
;
106 bool has_original
, has_target
;
113 assert(!e
->comment_paths
|| (e
->context
->marker_start
&& e
->context
->marker_end
));
118 r
= mkdir_parents_label(e
->path
, 0755);
120 return log_error_errno(r
, "Failed to create parent directories for '%s': %m", e
->path
);
122 r
= fopen_temporary_label(e
->path
, e
->path
, &f
, &temp
);
124 return log_error_errno(r
, "Failed to create temporary file for '%s': %m", e
->path
);
126 if (fchmod(fileno(f
), 0644) < 0)
127 return log_error_errno(errno
, "Failed to change mode of temporary file '%s': %m", temp
);
129 has_original
= e
->original_path
&& access(e
->original_path
, F_OK
) >= 0;
130 has_target
= access(e
->path
, F_OK
) >= 0;
132 if (has_original
&& (!has_target
|| e
->context
->overwrite_with_origin
))
133 /* We are asked to overwrite target with original_path or target doesn't exist. */
134 source
= e
->original_path
;
136 /* Target exists and shouldn't be overwritten. */
141 if (e
->comment_paths
) {
142 _cleanup_free_
char *source_contents
= NULL
;
145 r
= read_full_file(source
, &source_contents
, NULL
);
147 return log_error_errno(r
, "Failed to read source file '%s': %m", source
);
158 e
->context
->marker_start
,
159 strempty(source_contents
),
160 source_contents
&& endswith(source_contents
, "\n") ? "" : "\n",
161 e
->context
->marker_end
);
163 line
= 4; /* Start editing at the contents area */
165 STRV_FOREACH(path
, e
->comment_paths
) {
166 _cleanup_free_
char *comment
= NULL
;
168 /* Skip the file which is being edited and the source file (can be the same) */
169 if (PATH_IN_SET(*path
, e
->path
, source
))
172 r
= read_full_file(*path
, &comment
, NULL
);
174 return log_error_errno(r
, "Failed to read comment file '%s': %m", *path
);
176 fprintf(f
, "\n\n### %s", *path
);
178 if (!isempty(comment
)) {
179 _cleanup_free_
char *c
= NULL
;
181 c
= strreplace(strstrip(comment
), "\n", "\n# ");
185 fprintf(f
, "\n# %s", c
);
189 r
= copy_file_fd(source
, fileno(f
), COPY_REFLINK
);
191 assert(r
!= -ENOENT
);
192 return log_error_errno(r
, "Failed to copy file '%s' to temporary file '%s': %m", source
, temp
);
196 r
= fflush_and_check(f
);
198 return log_error_errno(r
, "Failed to write to temporary file '%s': %m", temp
);
200 e
->temp
= TAKE_PTR(temp
);
206 static int run_editor_child(const EditFileContext
*context
) {
207 _cleanup_strv_free_
char **args
= NULL
;
211 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
212 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
213 * well known editors. */
214 editor
= getenv("SYSTEMD_EDITOR");
216 editor
= getenv("EDITOR");
218 editor
= getenv("VISUAL");
220 if (!isempty(editor
)) {
221 _cleanup_strv_free_
char **editor_args
= NULL
;
223 editor_args
= strv_split(editor
, WHITESPACE
);
227 args
= TAKE_PTR(editor_args
);
230 if (context
->n_files
== 1 && context
->files
[0].line
> 1) {
231 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
232 r
= strv_extendf(&args
, "+%u", context
->files
[0].line
);
237 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
238 r
= strv_extend(&args
, i
->temp
);
243 if (!isempty(editor
))
244 execvp(args
[0], (char* const*) args
);
246 bool prepended
= false;
247 FOREACH_STRING(name
, "editor", "nano", "vim", "vi") {
249 r
= strv_prepend(&args
, name
);
252 r
= free_and_strdup(&args
[0], name
);
256 execvp(args
[0], (char* const*) args
);
258 /* We do not fail if the editor doesn't exist because we want to try each one of them
261 return log_error_errno(errno
, "Failed to execute '%s': %m", name
);
264 return log_error_errno(SYNTHETIC_ERRNO(ENOENT
),
265 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
268 static int run_editor(const EditFileContext
*context
) {
273 r
= safe_fork("(editor)", FORK_RESET_SIGNALS
|FORK_DEATHSIG_SIGTERM
|FORK_RLIMIT_NOFILE_SAFE
|FORK_LOG
|FORK_WAIT
, NULL
);
276 if (r
== 0) { /* Child */
277 r
= run_editor_child(context
);
278 _exit(r
< 0 ? EXIT_FAILURE
: EXIT_SUCCESS
);
284 static int strip_edit_temp_file(EditFile
*e
) {
285 _cleanup_free_
char *old_contents
= NULL
, *new_contents
= NULL
;
286 const char *stripped
;
293 r
= read_full_file(e
->temp
, &old_contents
, NULL
);
295 return log_error_errno(r
, "Failed to read temporary file '%s': %m", e
->temp
);
297 if (e
->context
->marker_start
) {
298 /* Trim out the lines between the two markers */
299 char *contents_start
, *contents_end
;
301 assert(e
->context
->marker_end
);
303 contents_start
= strstrafter(old_contents
, e
->context
->marker_start
);
305 contents_start
= old_contents
;
307 contents_end
= strstr(contents_start
, e
->context
->marker_end
);
309 *contents_end
= '\0';
311 stripped
= strstrip(contents_start
);
313 stripped
= strstrip(old_contents
);
314 if (isempty(stripped
))
315 return 0; /* File is empty (has no real changes) */
317 /* Trim prefix and suffix, but ensure suffixed by single newline */
318 new_contents
= strjoin(stripped
, "\n");
322 if (streq(old_contents
, new_contents
)) /* Don't touch the file if the above didn't change a thing */
323 return 1; /* Contents unchanged after stripping but has changes */
325 r
= write_string_file(e
->temp
, new_contents
, WRITE_STRING_FILE_CREATE
| WRITE_STRING_FILE_TRUNCATE
| WRITE_STRING_FILE_AVOID_NEWLINE
);
327 return log_error_errno(r
, "Failed to strip temporary file '%s': %m", e
->temp
);
329 return 1; /* Contents have real changes and are changed after stripping */
332 int do_edit_files_and_install(EditFileContext
*context
) {
337 if (context
->n_files
== 0)
338 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT
), "Got no files to edit.");
340 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
341 r
= create_edit_temp_file(i
);
346 r
= run_editor(context
);
350 FOREACH_ARRAY(i
, context
->files
, context
->n_files
) {
351 /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */
352 r
= strip_edit_temp_file(i
);
355 if (r
== 0) /* temp file doesn't carry actual changes, ignoring */
358 r
= RET_NERRNO(rename(i
->temp
, i
->path
));
360 return log_error_errno(r
,
361 "Failed to rename temporary file '%s' to target file '%s': %m",
364 i
->temp
= mfree(i
->temp
);
366 log_info("Successfully installed edited file '%s'.", i
->path
);