]>
Commit | Line | Data |
---|---|---|
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 |
21 | void 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 | ||
52 | bool 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 |
63 | int 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 | ||
107 | static 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 |
222 | static 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 |
284 | static 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 | 300 | static 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 | |
341 | int 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 | } |