]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/edit-util.c
systemctl: edit: several cleanups
[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
9a11b4f9 222static int run_editor(const EditFileContext *context) {
a01c4bc9
MY
223 int r;
224
9a11b4f9 225 assert(context);
a01c4bc9
MY
226
227 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
228 if (r < 0)
229 return r;
230 if (r == 0) {
231 size_t n_editor_args = 0, i = 1, argc;
232 char **editor_args = NULL, **args;
233 const char *editor;
234
235 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL. If
236 * neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute well known
237 * editors. */
238 editor = getenv("SYSTEMD_EDITOR");
239 if (!editor)
240 editor = getenv("EDITOR");
241 if (!editor)
242 editor = getenv("VISUAL");
243
244 if (isempty(editor))
245 argc = 1;
246 else {
247 editor_args = strv_split(editor, WHITESPACE);
248 if (!editor_args) {
249 (void) log_oom();
250 _exit(EXIT_FAILURE);
251 }
252 n_editor_args = strv_length(editor_args);
253 argc = n_editor_args;
254 }
255
9a11b4f9 256 argc += context->n_files * 2;
a01c4bc9
MY
257
258 args = newa(char*, argc + 1);
259
260 if (n_editor_args > 0) {
261 args[0] = editor_args[0];
262 for (; i < n_editor_args; i++)
263 args[i] = editor_args[i];
264 }
265
9a11b4f9 266 if (context->n_files == 1 && context->files[0].line > 1) {
a01c4bc9 267 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
9a11b4f9 268 if (asprintf(args + i, "+%u", context->files[0].line) < 0) {
a01c4bc9
MY
269 (void) log_oom();
270 _exit(EXIT_FAILURE);
271 }
272
273 i++;
9a11b4f9 274 args[i++] = context->files[0].temp;
a01c4bc9 275 } else
9a11b4f9
MY
276 FOREACH_ARRAY(f, context->files, context->n_files)
277 args[i++] = f->temp;
a01c4bc9
MY
278
279 args[i] = NULL;
280
281 if (n_editor_args > 0)
282 execvp(args[0], (char* const*) args);
283
284 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
285 args[0] = (char*) name;
286 execvp(name, (char* const*) args);
287 /* We do not fail if the editor doesn't exist because we want to try each one of them
288 * before failing. */
289 if (errno != ENOENT) {
290 log_error_errno(errno, "Failed to execute %s: %m", name);
291 _exit(EXIT_FAILURE);
292 }
293 }
294
fa2413dd 295 log_error("Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
a01c4bc9
MY
296 _exit(EXIT_FAILURE);
297 }
298
299 return 0;
300}
301
9a11b4f9 302static int trim_edit_markers(const char *path, const char *marker_start, const char *marker_end) {
a01c4bc9
MY
303 _cleanup_free_ char *old_contents = NULL, *new_contents = NULL;
304 char *contents_start, *contents_end;
305 const char *c = NULL;
306 int r;
307
fa2413dd
MY
308 assert(!marker_start == !marker_end);
309
a01c4bc9
MY
310 /* Trim out the lines between the two markers */
311 r = read_full_file(path, &old_contents, NULL);
312 if (r < 0)
313 return log_error_errno(r, "Failed to read temporary file \"%s\": %m", path);
314
315 contents_start = strstr(old_contents, marker_start);
316 if (contents_start)
317 contents_start += strlen(marker_start);
318 else
319 contents_start = old_contents;
320
321 contents_end = strstr(contents_start, marker_end);
322 if (contents_end)
323 contents_end[0] = 0;
324
325 c = strstrip(contents_start);
326 if (isempty(c))
327 return 0; /* All gone now */
328
329 new_contents = strjoin(c, "\n"); /* Trim prefix and suffix, but ensure suffixed by single newline */
330 if (!new_contents)
331 return log_oom();
332
333 if (streq(old_contents, new_contents)) /* Don't touch the file if the above didn't change a thing */
334 return 1; /* Unchanged, but good */
335
336 r = write_string_file(path, new_contents, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
337 if (r < 0)
338 return log_error_errno(r, "Failed to modify temporary file \"%s\": %m", path);
339
340 return 1; /* Changed, but good */
341}
9a11b4f9
MY
342
343int do_edit_files_and_install(EditFileContext *context) {
344 int r;
345
346 assert(context);
347
348 if (context->n_files == 0)
349 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
350
351 FOREACH_ARRAY(i, context->files, context->n_files)
352 if (isempty(i->temp)) {
353 r = create_edit_temp_file(i->path,
354 i->original_path,
355 i->comment_paths,
356 context->marker_start,
357 context->marker_end,
358 &i->temp,
359 &i->line);
360 if (r < 0)
361 return r;
362 }
363
364 r = run_editor(context);
365 if (r < 0)
366 return r;
367
368 FOREACH_ARRAY(i, context->files, context->n_files) {
369 /* Always call trim_edit_markers to tell if the temp file is empty */
370 r = trim_edit_markers(i->temp, context->marker_start, context->marker_end);
371 if (r < 0)
372 return r;
373 if (r == 0) /* temp file doesn't carry actual changes, ignoring */
374 continue;
375
376 r = RET_NERRNO(rename(i->temp, i->path));
377 if (r < 0)
378 return log_error_errno(r, "Failed to rename \"%s\" to \"%s\": %m", i->temp, i->path);
379 i->temp = mfree(i->temp);
380
381 log_info("Successfully installed edited file '%s'.", i->path);
382 }
383
384 return 0;
385}