]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/edit-util.c
process-util: add new FORK_DEATHSIG_SIGKILL flag, rename FORK_DEATHSIG → FORK_DEATHSI...
[thirdparty/systemd.git] / src / shared / edit-util.c
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 "string-util.h"
16 #include "strv.h"
17 #include "tmpfile-util-label.h"
18
19 void edit_file_context_done(EditFileContext *context) {
20 int r;
21
22 assert(context);
23
24 FOREACH_ARRAY(i, context->files, context->n_files) {
25 unlink_and_free(i->temp);
26
27 if (context->remove_parent) {
28 _cleanup_free_ char *parent = NULL;
29
30 r = path_extract_directory(i->path, &parent);
31 if (r < 0)
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);
35 }
36
37 free(i->path);
38 free(i->original_path);
39 strv_free(i->comment_paths);
40 }
41
42 context->files = mfree(context->files);
43 context->n_files = 0;
44 }
45
46 bool edit_files_contains(const EditFileContext *context, const char *path) {
47 assert(context);
48 assert(path);
49
50 FOREACH_ARRAY(i, context->files, context->n_files)
51 if (path_equal(i->path, path))
52 return true;
53
54 return false;
55 }
56
57 int edit_files_add(
58 EditFileContext *context,
59 const char *path,
60 const char *original_path,
61 char * const *comment_paths) {
62
63 _cleanup_free_ char *new_path = NULL, *new_original_path = NULL;
64 _cleanup_strv_free_ char **new_comment_paths = NULL;
65
66 assert(context);
67 assert(path);
68
69 if (edit_files_contains(context, path))
70 return 0;
71
72 if (!GREEDY_REALLOC(context->files, context->n_files + 1))
73 return log_oom();
74
75 new_path = strdup(path);
76 if (!new_path)
77 return log_oom();
78
79 if (original_path) {
80 new_original_path = strdup(original_path);
81 if (!new_original_path)
82 return log_oom();
83 }
84
85 if (comment_paths) {
86 new_comment_paths = strv_copy(comment_paths);
87 if (!new_comment_paths)
88 return log_oom();
89 }
90
91 context->files[context->n_files] = (EditFile) {
92 .context = context,
93 .path = TAKE_PTR(new_path),
94 .original_path = TAKE_PTR(new_original_path),
95 .comment_paths = TAKE_PTR(new_comment_paths),
96 };
97 context->n_files++;
98
99 return 1;
100 }
101
102 static int create_edit_temp_file(EditFile *e) {
103 _cleanup_(unlink_and_freep) char *temp = NULL;
104 _cleanup_fclose_ FILE *f = NULL;
105 const char *source;
106 bool has_original, has_target;
107 unsigned line = 1;
108 int r;
109
110 assert(e);
111 assert(e->context);
112 assert(e->path);
113 assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end));
114
115 if (e->temp)
116 return 0;
117
118 r = mkdir_parents_label(e->path, 0755);
119 if (r < 0)
120 return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path);
121
122 r = fopen_temporary_label(e->path, e->path, &f, &temp);
123 if (r < 0)
124 return log_error_errno(r, "Failed to create temporary file for '%s': %m", e->path);
125
126 if (fchmod(fileno(f), 0644) < 0)
127 return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp);
128
129 has_original = e->original_path && access(e->original_path, F_OK) >= 0;
130 has_target = access(e->path, F_OK) >= 0;
131
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;
135 else if (has_target)
136 /* Target exists and shouldn't be overwritten. */
137 source = e->path;
138 else
139 source = NULL;
140
141 if (e->comment_paths) {
142 _cleanup_free_ char *source_contents = NULL;
143
144 if (source) {
145 r = read_full_file(source, &source_contents, NULL);
146 if (r < 0)
147 return log_error_errno(r, "Failed to read source file '%s': %m", source);
148 }
149
150 fprintf(f,
151 "### Editing %s\n"
152 "%s\n"
153 "\n"
154 "%s%s"
155 "\n"
156 "%s\n",
157 e->path,
158 e->context->marker_start,
159 strempty(source_contents),
160 source_contents && endswith(source_contents, "\n") ? "" : "\n",
161 e->context->marker_end);
162
163 line = 4; /* Start editing at the contents area */
164
165 STRV_FOREACH(path, e->comment_paths) {
166 _cleanup_free_ char *comment = NULL;
167
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))
170 continue;
171
172 r = read_full_file(*path, &comment, NULL);
173 if (r < 0)
174 return log_error_errno(r, "Failed to read comment file '%s': %m", *path);
175
176 fprintf(f, "\n\n### %s", *path);
177
178 if (!isempty(comment)) {
179 _cleanup_free_ char *c = NULL;
180
181 c = strreplace(strstrip(comment), "\n", "\n# ");
182 if (!c)
183 return log_oom();
184
185 fprintf(f, "\n# %s", c);
186 }
187 }
188 } else if (source) {
189 r = copy_file_fd(source, fileno(f), COPY_REFLINK);
190 if (r < 0) {
191 assert(r != -ENOENT);
192 return log_error_errno(r, "Failed to copy file '%s' to temporary file '%s': %m", source, temp);
193 }
194 }
195
196 r = fflush_and_check(f);
197 if (r < 0)
198 return log_error_errno(r, "Failed to write to temporary file '%s': %m", temp);
199
200 e->temp = TAKE_PTR(temp);
201 e->line = line;
202
203 return 0;
204 }
205
206 static int run_editor_child(const EditFileContext *context) {
207 _cleanup_strv_free_ char **args = NULL;
208 const char *editor;
209 int r;
210
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");
215 if (!editor)
216 editor = getenv("EDITOR");
217 if (!editor)
218 editor = getenv("VISUAL");
219
220 if (!isempty(editor)) {
221 _cleanup_strv_free_ char **editor_args = NULL;
222
223 editor_args = strv_split(editor, WHITESPACE);
224 if (!editor_args)
225 return log_oom();
226
227 args = TAKE_PTR(editor_args);
228 }
229
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);
233 if (r < 0)
234 return log_oom();
235 }
236
237 FOREACH_ARRAY(i, context->files, context->n_files) {
238 r = strv_extend(&args, i->temp);
239 if (r < 0)
240 return log_oom();
241 }
242
243 if (!isempty(editor))
244 execvp(args[0], (char* const*) args);
245
246 bool prepended = false;
247 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
248 if (!prepended) {
249 r = strv_prepend(&args, name);
250 prepended = true;
251 } else
252 r = free_and_strdup(&args[0], name);
253 if (r < 0)
254 return log_oom();
255
256 execvp(args[0], (char* const*) args);
257
258 /* We do not fail if the editor doesn't exist because we want to try each one of them
259 * before failing. */
260 if (errno != ENOENT)
261 return log_error_errno(errno, "Failed to execute '%s': %m", name);
262 }
263
264 return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
265 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
266 }
267
268 static int run_editor(const EditFileContext *context) {
269 int r;
270
271 assert(context);
272
273 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
274 if (r < 0)
275 return r;
276 if (r == 0) { /* Child */
277 r = run_editor_child(context);
278 _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
279 }
280
281 return 0;
282 }
283
284 static int strip_edit_temp_file(EditFile *e) {
285 _cleanup_free_ char *old_contents = NULL, *new_contents = NULL;
286 const char *stripped;
287 int r;
288
289 assert(e);
290 assert(e->context);
291 assert(e->temp);
292
293 r = read_full_file(e->temp, &old_contents, NULL);
294 if (r < 0)
295 return log_error_errno(r, "Failed to read temporary file '%s': %m", e->temp);
296
297 if (e->context->marker_start) {
298 /* Trim out the lines between the two markers */
299 char *contents_start, *contents_end;
300
301 assert(e->context->marker_end);
302
303 contents_start = strstrafter(old_contents, e->context->marker_start);
304 if (!contents_start)
305 contents_start = old_contents;
306
307 contents_end = strstr(contents_start, e->context->marker_end);
308 if (contents_end)
309 *contents_end = '\0';
310
311 stripped = strstrip(contents_start);
312 } else
313 stripped = strstrip(old_contents);
314 if (isempty(stripped))
315 return 0; /* File is empty (has no real changes) */
316
317 /* Trim prefix and suffix, but ensure suffixed by single newline */
318 new_contents = strjoin(stripped, "\n");
319 if (!new_contents)
320 return log_oom();
321
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 */
324
325 r = write_string_file(e->temp, new_contents, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
326 if (r < 0)
327 return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp);
328
329 return 1; /* Contents have real changes and are changed after stripping */
330 }
331
332 int do_edit_files_and_install(EditFileContext *context) {
333 int r;
334
335 assert(context);
336
337 if (context->n_files == 0)
338 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
339
340 FOREACH_ARRAY(i, context->files, context->n_files) {
341 r = create_edit_temp_file(i);
342 if (r < 0)
343 return r;
344 }
345
346 r = run_editor(context);
347 if (r < 0)
348 return r;
349
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);
353 if (r < 0)
354 return r;
355 if (r == 0) /* temp file doesn't carry actual changes, ignoring */
356 continue;
357
358 r = RET_NERRNO(rename(i->temp, i->path));
359 if (r < 0)
360 return log_error_errno(r,
361 "Failed to rename temporary file '%s' to target file '%s': %m",
362 i->temp,
363 i->path);
364 i->temp = mfree(i->temp);
365
366 log_info("Successfully installed edited file '%s'.", i->path);
367 }
368
369 return 0;
370 }