]>
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" | |
a01c4bc9 MY |
15 | #include "string-util.h" |
16 | #include "strv.h" | |
54ad6aa1 | 17 | #include "tmpfile-util-label.h" |
a01c4bc9 | 18 | |
9a11b4f9 MY |
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) { | |
8992667f | 25 | unlink_and_free(i->temp); |
9a11b4f9 MY |
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); | |
d3bf024f MY |
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); | |
9a11b4f9 | 35 | } |
a01c4bc9 | 36 | |
a01c4bc9 | 37 | free(i->path); |
9a11b4f9 MY |
38 | free(i->original_path); |
39 | strv_free(i->comment_paths); | |
a01c4bc9 MY |
40 | } |
41 | ||
9a11b4f9 MY |
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) | |
00b6baf2 | 51 | if (path_equal(i->path, path)) |
9a11b4f9 MY |
52 | return true; |
53 | ||
54 | return false; | |
a01c4bc9 MY |
55 | } |
56 | ||
9a11b4f9 MY |
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 | ||
da037170 | 72 | if (!GREEDY_REALLOC(context->files, context->n_files + 1)) |
9a11b4f9 MY |
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) { | |
043db340 | 92 | .context = context, |
9a11b4f9 MY |
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 | ||
c629a5bc | 102 | static int create_edit_temp_file(EditFile *e) { |
0a742f36 | 103 | _cleanup_(unlink_and_freep) char *temp = NULL; |
54ad6aa1 MY |
104 | _cleanup_fclose_ FILE *f = NULL; |
105 | const char *source; | |
106 | bool has_original, has_target; | |
fa2413dd | 107 | unsigned line = 1; |
a01c4bc9 MY |
108 | int r; |
109 | ||
c629a5bc YW |
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; | |
a01c4bc9 | 117 | |
54ad6aa1 | 118 | r = mkdir_parents_label(e->path, 0755); |
a01c4bc9 | 119 | if (r < 0) |
54ad6aa1 | 120 | return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path); |
a01c4bc9 | 121 | |
54ad6aa1 | 122 | r = fopen_temporary_label(e->path, e->path, &f, &temp); |
a01c4bc9 | 123 | if (r < 0) |
54ad6aa1 | 124 | return log_error_errno(r, "Failed to create temporary file for '%s': %m", e->path); |
a01c4bc9 | 125 | |
54ad6aa1 MY |
126 | if (fchmod(fileno(f), 0644) < 0) |
127 | return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp); | |
52073ba2 | 128 | |
54ad6aa1 MY |
129 | has_original = e->original_path && access(e->original_path, F_OK) >= 0; |
130 | has_target = access(e->path, F_OK) >= 0; | |
52073ba2 | 131 | |
bc6c7a58 MY |
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. */ | |
54ad6aa1 MY |
134 | source = e->original_path; |
135 | else if (has_target) | |
bc6c7a58 | 136 | /* Target exists and shouldn't be overwritten. */ |
54ad6aa1 MY |
137 | source = e->path; |
138 | else | |
139 | source = NULL; | |
fa2413dd | 140 | |
c629a5bc | 141 | if (e->comment_paths) { |
54ad6aa1 | 142 | _cleanup_free_ char *source_contents = NULL; |
a01c4bc9 | 143 | |
54ad6aa1 MY |
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 | } | |
a01c4bc9 MY |
149 | |
150 | fprintf(f, | |
151 | "### Editing %s\n" | |
152 | "%s\n" | |
153 | "\n" | |
154 | "%s%s" | |
155 | "\n" | |
156 | "%s\n", | |
c629a5bc YW |
157 | e->path, |
158 | e->context->marker_start, | |
54ad6aa1 MY |
159 | strempty(source_contents), |
160 | source_contents && endswith(source_contents, "\n") ? "" : "\n", | |
c629a5bc | 161 | e->context->marker_end); |
a01c4bc9 | 162 | |
fa2413dd | 163 | line = 4; /* Start editing at the contents area */ |
a01c4bc9 | 164 | |
c629a5bc | 165 | STRV_FOREACH(path, e->comment_paths) { |
54ad6aa1 | 166 | _cleanup_free_ char *comment = NULL; |
a01c4bc9 | 167 | |
54ad6aa1 MY |
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)) | |
a01c4bc9 MY |
170 | continue; |
171 | ||
54ad6aa1 | 172 | r = read_full_file(*path, &comment, NULL); |
a01c4bc9 | 173 | if (r < 0) |
54ad6aa1 | 174 | return log_error_errno(r, "Failed to read comment file '%s': %m", *path); |
a01c4bc9 MY |
175 | |
176 | fprintf(f, "\n\n### %s", *path); | |
a01c4bc9 | 177 | |
54ad6aa1 MY |
178 | if (!isempty(comment)) { |
179 | _cleanup_free_ char *c = NULL; | |
180 | ||
181 | c = strreplace(strstrip(comment), "\n", "\n# "); | |
182 | if (!c) | |
a01c4bc9 | 183 | return log_oom(); |
fa2413dd | 184 | |
54ad6aa1 | 185 | fprintf(f, "\n# %s", c); |
a01c4bc9 MY |
186 | } |
187 | } | |
54ad6aa1 MY |
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 | } | |
a01c4bc9 MY |
194 | } |
195 | ||
54ad6aa1 MY |
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 | ||
c629a5bc YW |
200 | e->temp = TAKE_PTR(temp); |
201 | e->line = line; | |
a01c4bc9 MY |
202 | |
203 | return 0; | |
204 | } | |
205 | ||
f8970df5 MY |
206 | static int run_editor_child(const EditFileContext *context) { |
207 | _cleanup_strv_free_ char **args = NULL; | |
208 | const char *editor; | |
a01c4bc9 MY |
209 | int r; |
210 | ||
f8970df5 MY |
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"); | |
a01c4bc9 | 219 | |
f8970df5 MY |
220 | if (!isempty(editor)) { |
221 | _cleanup_strv_free_ char **editor_args = NULL; | |
a01c4bc9 | 222 | |
f8970df5 MY |
223 | editor_args = strv_split(editor, WHITESPACE); |
224 | if (!editor_args) | |
225 | return log_oom(); | |
a01c4bc9 | 226 | |
f8970df5 MY |
227 | args = TAKE_PTR(editor_args); |
228 | } | |
a01c4bc9 | 229 | |
f8970df5 MY |
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 | } | |
a01c4bc9 | 236 | |
f8970df5 MY |
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 | } | |
a01c4bc9 | 242 | |
f8970df5 MY |
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; | |
a01c4bc9 | 251 | } else |
f8970df5 MY |
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 | } | |
a01c4bc9 | 267 | |
f8970df5 MY |
268 | static int run_editor(const EditFileContext *context) { |
269 | int r; | |
270 | ||
271 | assert(context); | |
272 | ||
e9ccae31 | 273 | r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL); |
f8970df5 MY |
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); | |
a01c4bc9 MY |
279 | } |
280 | ||
281 | return 0; | |
282 | } | |
283 | ||
978e222f | 284 | static int strip_edit_temp_file(EditFile *e) { |
a01c4bc9 | 285 | _cleanup_free_ char *old_contents = NULL, *new_contents = NULL; |
978e222f | 286 | const char *stripped; |
a01c4bc9 MY |
287 | int r; |
288 | ||
aec8200b YW |
289 | assert(e); |
290 | assert(e->context); | |
291 | assert(e->temp); | |
fa2413dd | 292 | |
aec8200b | 293 | r = read_full_file(e->temp, &old_contents, NULL); |
a01c4bc9 | 294 | if (r < 0) |
77c9bb17 | 295 | return log_error_errno(r, "Failed to read temporary file '%s': %m", e->temp); |
aec8200b YW |
296 | |
297 | if (e->context->marker_start) { | |
978e222f | 298 | /* Trim out the lines between the two markers */ |
aec8200b YW |
299 | char *contents_start, *contents_end; |
300 | ||
301 | assert(e->context->marker_end); | |
a01c4bc9 | 302 | |
d791013f LP |
303 | contents_start = strstrafter(old_contents, e->context->marker_start); |
304 | if (!contents_start) | |
aec8200b | 305 | contents_start = old_contents; |
a01c4bc9 | 306 | |
aec8200b YW |
307 | contents_end = strstr(contents_start, e->context->marker_end); |
308 | if (contents_end) | |
978e222f | 309 | *contents_end = '\0'; |
a01c4bc9 | 310 | |
978e222f | 311 | stripped = strstrip(contents_start); |
aec8200b | 312 | } else |
978e222f MY |
313 | stripped = strstrip(old_contents); |
314 | if (isempty(stripped)) | |
315 | return 0; /* File is empty (has no real changes) */ | |
a01c4bc9 | 316 | |
978e222f MY |
317 | /* Trim prefix and suffix, but ensure suffixed by single newline */ |
318 | new_contents = strjoin(stripped, "\n"); | |
a01c4bc9 MY |
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 */ | |
978e222f | 323 | return 1; /* Contents unchanged after stripping but has changes */ |
a01c4bc9 | 324 | |
aec8200b | 325 | r = write_string_file(e->temp, new_contents, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE); |
a01c4bc9 | 326 | if (r < 0) |
77c9bb17 | 327 | return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp); |
a01c4bc9 | 328 | |
978e222f | 329 | return 1; /* Contents have real changes and are changed after stripping */ |
a01c4bc9 | 330 | } |
9a11b4f9 MY |
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 | ||
c629a5bc YW |
340 | FOREACH_ARRAY(i, context->files, context->n_files) { |
341 | r = create_edit_temp_file(i); | |
342 | if (r < 0) | |
343 | return r; | |
344 | } | |
9a11b4f9 MY |
345 | |
346 | r = run_editor(context); | |
347 | if (r < 0) | |
348 | return r; | |
349 | ||
350 | FOREACH_ARRAY(i, context->files, context->n_files) { | |
978e222f MY |
351 | /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */ |
352 | r = strip_edit_temp_file(i); | |
9a11b4f9 MY |
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) | |
77c9bb17 MY |
360 | return log_error_errno(r, |
361 | "Failed to rename temporary file '%s' to target file '%s': %m", | |
362 | i->temp, | |
363 | i->path); | |
9a11b4f9 MY |
364 | i->temp = mfree(i->temp); |
365 | ||
366 | log_info("Successfully installed edited file '%s'.", i->path); | |
367 | } | |
368 | ||
369 | return 0; | |
370 | } |