]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/edit-util.c
systemctl-edit: invert one error check
[thirdparty/systemd.git] / src / shared / edit-util.c
CommitLineData
a01c4bc9
MY
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include <errno.h>
4#include <stdio.h>
5
6#include "alloc-util.h"
7#include "copy.h"
8#include "edit-util.h"
9#include "fd-util.h"
10#include "fileio.h"
11#include "fs-util.h"
12#include "mkdir-label.h"
13#include "path-util.h"
14#include "process-util.h"
15#include "selinux-util.h"
16#include "stat-util.h"
17#include "string-util.h"
18#include "strv.h"
19#include "tmpfile-util.h"
20
9a11b4f9
MY
21void edit_file_context_done(EditFileContext *context) {
22 int r;
23
24 assert(context);
25
26 FOREACH_ARRAY(i, context->files, context->n_files) {
27 if (i->temp) {
28 (void) unlink(i->temp);
29 free(i->temp);
30 }
31
32 if (context->remove_parent) {
33 _cleanup_free_ char *parent = NULL;
34
35 r = path_extract_directory(i->path, &parent);
36 if (r < 0)
37 log_debug_errno(r, "Failed to extract directory from '%s', ignoring: %m", i->path);
38
39 /* No need to check if the dir is empty, rmdir does nothing if it is not the case. */
40 (void) rmdir(parent);
41 }
a01c4bc9 42
a01c4bc9 43 free(i->path);
9a11b4f9
MY
44 free(i->original_path);
45 strv_free(i->comment_paths);
a01c4bc9
MY
46 }
47
9a11b4f9
MY
48 context->files = mfree(context->files);
49 context->n_files = 0;
50}
51
52bool edit_files_contains(const EditFileContext *context, const char *path) {
53 assert(context);
54 assert(path);
55
56 FOREACH_ARRAY(i, context->files, context->n_files)
57 if (streq(i->path, path))
58 return true;
59
60 return false;
a01c4bc9
MY
61}
62
9a11b4f9
MY
63int edit_files_add(
64 EditFileContext *context,
65 const char *path,
66 const char *original_path,
67 char * const *comment_paths) {
68
69 _cleanup_free_ char *new_path = NULL, *new_original_path = NULL;
70 _cleanup_strv_free_ char **new_comment_paths = NULL;
71
72 assert(context);
73 assert(path);
74
75 if (edit_files_contains(context, path))
76 return 0;
77
78 if (!GREEDY_REALLOC0(context->files, context->n_files + 2))
79 return log_oom();
80
81 new_path = strdup(path);
82 if (!new_path)
83 return log_oom();
84
85 if (original_path) {
86 new_original_path = strdup(original_path);
87 if (!new_original_path)
88 return log_oom();
89 }
90
91 if (comment_paths) {
92 new_comment_paths = strv_copy(comment_paths);
93 if (!new_comment_paths)
94 return log_oom();
95 }
96
97 context->files[context->n_files] = (EditFile) {
98 .path = TAKE_PTR(new_path),
99 .original_path = TAKE_PTR(new_original_path),
100 .comment_paths = TAKE_PTR(new_comment_paths),
101 };
102 context->n_files++;
103
104 return 1;
105}
106
107static int create_edit_temp_file(
fa2413dd 108 const char *target_path,
a01c4bc9 109 const char *original_path,
fa2413dd 110 char * const *comment_paths,
a01c4bc9
MY
111 const char *marker_start,
112 const char *marker_end,
fa2413dd 113 char **ret_temp_filename,
a01c4bc9
MY
114 unsigned *ret_edit_line) {
115
fa2413dd
MY
116 _cleanup_free_ char *temp = NULL;
117 unsigned line = 1;
a01c4bc9
MY
118 int r;
119
fa2413dd
MY
120 assert(target_path);
121 assert(!comment_paths || (marker_start && marker_end));
122 assert(ret_temp_filename);
a01c4bc9 123
fa2413dd 124 r = tempfn_random(target_path, NULL, &temp);
a01c4bc9 125 if (r < 0)
fa2413dd 126 return log_error_errno(r, "Failed to determine temporary filename for \"%s\": %m", target_path);
a01c4bc9 127
fa2413dd 128 r = mkdir_parents_label(target_path, 0755);
a01c4bc9 129 if (r < 0)
fa2413dd 130 return log_error_errno(r, "Failed to create parent directories for \"%s\": %m", target_path);
a01c4bc9
MY
131
132 if (original_path) {
fa2413dd 133 r = mac_selinux_create_file_prepare(target_path, S_IFREG);
a01c4bc9
MY
134 if (r < 0)
135 return r;
136
fa2413dd 137 r = copy_file(original_path, temp, 0, 0644, 0, 0, COPY_REFLINK);
a01c4bc9 138 if (r == -ENOENT) {
fa2413dd 139 r = touch(temp);
a01c4bc9
MY
140 mac_selinux_create_file_clear();
141 if (r < 0)
fa2413dd 142 return log_error_errno(r, "Failed to create temporary file \"%s\": %m", temp);
a01c4bc9
MY
143 } else {
144 mac_selinux_create_file_clear();
145 if (r < 0)
fa2413dd 146 return log_error_errno(r, "Failed to create temporary file for \"%s\": %m", target_path);
a01c4bc9 147 }
fa2413dd
MY
148 }
149
150 if (comment_paths) {
151 _cleanup_free_ char *target_contents = NULL;
a01c4bc9
MY
152 _cleanup_fclose_ FILE *f = NULL;
153
fa2413dd 154 r = mac_selinux_create_file_prepare(target_path, S_IFREG);
a01c4bc9
MY
155 if (r < 0)
156 return r;
157
fa2413dd 158 f = fopen(temp, "we");
a01c4bc9
MY
159 mac_selinux_create_file_clear();
160 if (!f)
fa2413dd 161 return log_error_errno(errno, "Failed to open temporary file \"%s\": %m", temp);
a01c4bc9
MY
162
163 if (fchmod(fileno(f), 0644) < 0)
fa2413dd 164 return log_error_errno(errno, "Failed to change mode of temporary file \"%s\": %m", temp);
a01c4bc9 165
fa2413dd 166 r = read_full_file(target_path, &target_contents, NULL);
a01c4bc9 167 if (r < 0 && r != -ENOENT)
fa2413dd 168 return log_error_errno(r, "Failed to read target file \"%s\": %m", target_path);
a01c4bc9
MY
169
170 fprintf(f,
171 "### Editing %s\n"
172 "%s\n"
173 "\n"
174 "%s%s"
175 "\n"
176 "%s\n",
fa2413dd
MY
177 target_path,
178 marker_start,
179 strempty(target_contents),
180 target_contents && endswith(target_contents, "\n") ? "" : "\n",
181 marker_end);
a01c4bc9 182
fa2413dd 183 line = 4; /* Start editing at the contents area */
a01c4bc9 184
fa2413dd
MY
185 /* Add a comment with the contents of the original files */
186 STRV_FOREACH(path, comment_paths) {
a01c4bc9
MY
187 _cleanup_free_ char *contents = NULL;
188
fa2413dd
MY
189 /* Skip the file that's being edited, already processed in above */
190 if (path_equal(*path, target_path))
a01c4bc9
MY
191 continue;
192
193 r = read_full_file(*path, &contents, NULL);
194 if (r < 0)
fa2413dd 195 return log_error_errno(r, "Failed to read original file \"%s\": %m", *path);
a01c4bc9
MY
196
197 fprintf(f, "\n\n### %s", *path);
198 if (!isempty(contents)) {
199 _cleanup_free_ char *commented_contents = NULL;
200
201 commented_contents = strreplace(strstrip(contents), "\n", "\n# ");
202 if (!commented_contents)
203 return log_oom();
fa2413dd 204
a01c4bc9
MY
205 fprintf(f, "\n# %s", commented_contents);
206 }
207 }
208
209 r = fflush_and_check(f);
210 if (r < 0)
fa2413dd 211 return log_error_errno(r, "Failed to create temporary file \"%s\": %m", temp);
a01c4bc9
MY
212 }
213
fa2413dd
MY
214 *ret_temp_filename = TAKE_PTR(temp);
215
216 if (ret_edit_line)
217 *ret_edit_line = line;
a01c4bc9
MY
218
219 return 0;
220}
221
f8970df5
MY
222static int run_editor_child(const EditFileContext *context) {
223 _cleanup_strv_free_ char **args = NULL;
224 const char *editor;
a01c4bc9
MY
225 int r;
226
f8970df5
MY
227 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
228 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
229 * well known editors. */
230 editor = getenv("SYSTEMD_EDITOR");
231 if (!editor)
232 editor = getenv("EDITOR");
233 if (!editor)
234 editor = getenv("VISUAL");
a01c4bc9 235
f8970df5
MY
236 if (!isempty(editor)) {
237 _cleanup_strv_free_ char **editor_args = NULL;
a01c4bc9 238
f8970df5
MY
239 editor_args = strv_split(editor, WHITESPACE);
240 if (!editor_args)
241 return log_oom();
a01c4bc9 242
f8970df5
MY
243 args = TAKE_PTR(editor_args);
244 }
a01c4bc9 245
f8970df5
MY
246 if (context->n_files == 1 && context->files[0].line > 1) {
247 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
248 r = strv_extendf(&args, "+%u", context->files[0].line);
249 if (r < 0)
250 return log_oom();
251 }
a01c4bc9 252
f8970df5
MY
253 FOREACH_ARRAY(i, context->files, context->n_files) {
254 r = strv_extend(&args, i->temp);
255 if (r < 0)
256 return log_oom();
257 }
a01c4bc9 258
f8970df5
MY
259 if (!isempty(editor))
260 execvp(args[0], (char* const*) args);
261
262 bool prepended = false;
263 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
264 if (!prepended) {
265 r = strv_prepend(&args, name);
266 prepended = true;
a01c4bc9 267 } else
f8970df5
MY
268 r = free_and_strdup(&args[0], name);
269 if (r < 0)
270 return log_oom();
271
272 execvp(args[0], (char* const*) args);
273
274 /* We do not fail if the editor doesn't exist because we want to try each one of them
275 * before failing. */
276 if (errno != ENOENT)
277 return log_error_errno(errno, "Failed to execute '%s': %m", name);
278 }
279
280 return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
281 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
282}
a01c4bc9 283
f8970df5
MY
284static int run_editor(const EditFileContext *context) {
285 int r;
286
287 assert(context);
288
289 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
290 if (r < 0)
291 return r;
292 if (r == 0) { /* Child */
293 r = run_editor_child(context);
294 _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
a01c4bc9
MY
295 }
296
297 return 0;
298}
299
9a11b4f9 300static int trim_edit_markers(const char *path, const char *marker_start, const char *marker_end) {
a01c4bc9
MY
301 _cleanup_free_ char *old_contents = NULL, *new_contents = NULL;
302 char *contents_start, *contents_end;
303 const char *c = NULL;
304 int r;
305
fa2413dd
MY
306 assert(!marker_start == !marker_end);
307
a01c4bc9
MY
308 /* Trim out the lines between the two markers */
309 r = read_full_file(path, &old_contents, NULL);
310 if (r < 0)
311 return log_error_errno(r, "Failed to read temporary file \"%s\": %m", path);
312
313 contents_start = strstr(old_contents, marker_start);
314 if (contents_start)
315 contents_start += strlen(marker_start);
316 else
317 contents_start = old_contents;
318
319 contents_end = strstr(contents_start, marker_end);
320 if (contents_end)
321 contents_end[0] = 0;
322
323 c = strstrip(contents_start);
324 if (isempty(c))
325 return 0; /* All gone now */
326
327 new_contents = strjoin(c, "\n"); /* Trim prefix and suffix, but ensure suffixed by single newline */
328 if (!new_contents)
329 return log_oom();
330
331 if (streq(old_contents, new_contents)) /* Don't touch the file if the above didn't change a thing */
332 return 1; /* Unchanged, but good */
333
334 r = write_string_file(path, new_contents, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
335 if (r < 0)
336 return log_error_errno(r, "Failed to modify temporary file \"%s\": %m", path);
337
338 return 1; /* Changed, but good */
339}
9a11b4f9
MY
340
341int do_edit_files_and_install(EditFileContext *context) {
342 int r;
343
344 assert(context);
345
346 if (context->n_files == 0)
347 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
348
349 FOREACH_ARRAY(i, context->files, context->n_files)
350 if (isempty(i->temp)) {
351 r = create_edit_temp_file(i->path,
352 i->original_path,
353 i->comment_paths,
354 context->marker_start,
355 context->marker_end,
356 &i->temp,
357 &i->line);
358 if (r < 0)
359 return r;
360 }
361
362 r = run_editor(context);
363 if (r < 0)
364 return r;
365
366 FOREACH_ARRAY(i, context->files, context->n_files) {
367 /* Always call trim_edit_markers to tell if the temp file is empty */
368 r = trim_edit_markers(i->temp, context->marker_start, context->marker_end);
369 if (r < 0)
370 return r;
371 if (r == 0) /* temp file doesn't carry actual changes, ignoring */
372 continue;
373
374 r = RET_NERRNO(rename(i->temp, i->path));
375 if (r < 0)
376 return log_error_errno(r, "Failed to rename \"%s\" to \"%s\": %m", i->temp, i->path);
377 i->temp = mfree(i->temp);
378
379 log_info("Successfully installed edited file '%s'.", i->path);
380 }
381
382 return 0;
383}