]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/edit-util.c
hwdb: Add mapping for Xiaomi Mipad 2 bottom bezel capacitive buttons
[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 typedef struct EditFile {
20 EditFileContext *context;
21 char *path;
22 char *original_path;
23 char **comment_paths;
24 char *temp;
25 unsigned line;
26 } EditFile;
27
28 void edit_file_context_done(EditFileContext *context) {
29 int r;
30
31 assert(context);
32
33 FOREACH_ARRAY(i, context->files, context->n_files) {
34 unlink_and_free(i->temp);
35
36 if (context->remove_parent) {
37 _cleanup_free_ char *parent = NULL;
38
39 r = path_extract_directory(i->path, &parent);
40 if (r < 0)
41 log_debug_errno(r, "Failed to extract directory from '%s', ignoring: %m", i->path);
42 else if (rmdir(parent) < 0 && !IN_SET(errno, ENOENT, ENOTEMPTY))
43 log_debug_errno(errno, "Failed to remove parent directory '%s', ignoring: %m", parent);
44 }
45
46 free(i->path);
47 free(i->original_path);
48 strv_free(i->comment_paths);
49 }
50
51 context->files = mfree(context->files);
52 context->n_files = 0;
53 }
54
55 bool edit_files_contains(const EditFileContext *context, const char *path) {
56 assert(context);
57 assert(path);
58
59 FOREACH_ARRAY(i, context->files, context->n_files)
60 if (path_equal(i->path, path))
61 return true;
62
63 return false;
64 }
65
66 int edit_files_add(
67 EditFileContext *context,
68 const char *path,
69 const char *original_path,
70 char * const *comment_paths) {
71
72 _cleanup_free_ char *new_path = NULL, *new_original_path = NULL;
73 _cleanup_strv_free_ char **new_comment_paths = NULL;
74
75 assert(context);
76 assert(path);
77
78 if (edit_files_contains(context, path))
79 return 0;
80
81 if (!GREEDY_REALLOC(context->files, context->n_files + 1))
82 return log_oom();
83
84 new_path = strdup(path);
85 if (!new_path)
86 return log_oom();
87
88 if (original_path) {
89 new_original_path = strdup(original_path);
90 if (!new_original_path)
91 return log_oom();
92 }
93
94 if (comment_paths) {
95 new_comment_paths = strv_copy(comment_paths);
96 if (!new_comment_paths)
97 return log_oom();
98 }
99
100 context->files[context->n_files] = (EditFile) {
101 .context = context,
102 .path = TAKE_PTR(new_path),
103 .original_path = TAKE_PTR(new_original_path),
104 .comment_paths = TAKE_PTR(new_comment_paths),
105 .line = 1,
106 };
107 context->n_files++;
108
109 return 1;
110 }
111
112 static int populate_edit_temp_file(EditFile *e, FILE *f, const char *filename) {
113 assert(e);
114 assert(f);
115 assert(filename);
116
117 bool has_original = e->original_path && access(e->original_path, F_OK) >= 0;
118 bool has_target = access(e->path, F_OK) >= 0;
119 const char *source;
120 int r;
121
122 if (has_original && (!has_target || e->context->overwrite_with_origin))
123 /* We are asked to overwrite target with original_path or target doesn't exist. */
124 source = e->original_path;
125 else if (has_target)
126 /* Target exists and shouldn't be overwritten. */
127 source = e->path;
128 else
129 source = NULL;
130
131 if (e->comment_paths) {
132 _cleanup_free_ char *source_contents = NULL;
133
134 if (source) {
135 r = read_full_file(source, &source_contents, NULL);
136 if (r < 0)
137 return log_error_errno(r, "Failed to read source file '%s': %m", source);
138 }
139
140 fprintf(f,
141 "### Editing %s\n"
142 "%s\n"
143 "\n"
144 "%s%s"
145 "\n"
146 "%s\n",
147 e->path,
148 e->context->marker_start,
149 strempty(source_contents),
150 source_contents && endswith(source_contents, "\n") ? "" : "\n",
151 e->context->marker_end);
152
153 e->line = 4; /* Start editing at the contents area */
154
155 STRV_FOREACH(path, e->comment_paths) {
156 _cleanup_free_ char *comment = NULL;
157
158 /* Skip the file which is being edited and the source file (can be the same) */
159 if (PATH_IN_SET(*path, e->path, source))
160 continue;
161
162 r = read_full_file(*path, &comment, NULL);
163 if (r < 0)
164 return log_error_errno(r, "Failed to read comment file '%s': %m", *path);
165
166 fprintf(f, "\n\n### %s", *path);
167
168 if (!isempty(comment)) {
169 _cleanup_free_ char *c = NULL;
170
171 c = strreplace(strstrip(comment), "\n", "\n# ");
172 if (!c)
173 return log_oom();
174
175 fprintf(f, "\n# %s", c);
176 }
177 }
178 } else if (source) {
179 r = copy_file_fd(source, fileno(f), COPY_REFLINK);
180 if (r < 0) {
181 assert(r != -ENOENT);
182 return log_error_errno(r, "Failed to copy file '%s' to temporary file '%s': %m",
183 source, filename);
184 }
185 }
186
187 return 0;
188 }
189
190 static int create_edit_temp_file(EditFile *e, const char *contents, size_t contents_size) {
191 _cleanup_(unlink_and_freep) char *temp = NULL;
192 _cleanup_fclose_ FILE *f = NULL;
193 int r;
194
195 assert(e);
196 assert(e->context);
197 assert(e->path);
198 assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end));
199 assert(contents || contents_size == 0);
200
201 if (e->temp)
202 return 0;
203
204 r = mkdir_parents_label(e->path, 0755);
205 if (r < 0)
206 return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path);
207
208 r = fopen_temporary_label(e->path, e->path, &f, &temp);
209 if (r < 0)
210 return log_error_errno(r, "Failed to create temporary file for '%s': %m", e->path);
211
212 if (fchmod(fileno(f), 0644) < 0)
213 return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp);
214
215 if (e->context->stdin) {
216 if (fwrite(contents, 1, contents_size, f) != contents_size)
217 return log_error_errno(SYNTHETIC_ERRNO(EIO),
218 "Failed to copy input to temporary file '%s'.", temp);
219 } else {
220 r = populate_edit_temp_file(e, f, temp);
221 if (r < 0)
222 return r;
223 }
224
225 r = fflush_and_check(f);
226 if (r < 0)
227 return log_error_errno(r, "Failed to write to temporary file '%s': %m", temp);
228
229 e->temp = TAKE_PTR(temp);
230
231 return 0;
232 }
233
234 static int run_editor_child(const EditFileContext *context) {
235 _cleanup_strv_free_ char **args = NULL;
236 const char *editor;
237 int r;
238
239 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
240 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
241 * well known editors. */
242 editor = getenv("SYSTEMD_EDITOR");
243 if (!editor)
244 editor = getenv("EDITOR");
245 if (!editor)
246 editor = getenv("VISUAL");
247
248 if (!isempty(editor)) {
249 _cleanup_strv_free_ char **editor_args = NULL;
250
251 editor_args = strv_split(editor, WHITESPACE);
252 if (!editor_args)
253 return log_oom();
254
255 args = TAKE_PTR(editor_args);
256 }
257
258 if (context->n_files == 1 && context->files[0].line > 1) {
259 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
260 r = strv_extendf(&args, "+%u", context->files[0].line);
261 if (r < 0)
262 return log_oom();
263 }
264
265 FOREACH_ARRAY(i, context->files, context->n_files) {
266 r = strv_extend(&args, i->temp);
267 if (r < 0)
268 return log_oom();
269 }
270
271 if (!isempty(editor))
272 execvp(args[0], (char* const*) args);
273
274 bool prepended = false;
275 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
276 if (!prepended) {
277 r = strv_prepend(&args, name);
278 prepended = true;
279 } else
280 r = free_and_strdup(&args[0], name);
281 if (r < 0)
282 return log_oom();
283
284 execvp(args[0], (char* const*) args);
285
286 /* We do not fail if the editor doesn't exist because we want to try each one of them
287 * before failing. */
288 if (errno != ENOENT)
289 return log_error_errno(errno, "Failed to execute '%s': %m", name);
290 }
291
292 return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
293 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
294 }
295
296 static int run_editor(const EditFileContext *context) {
297 int r;
298
299 assert(context);
300
301 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
302 if (r < 0)
303 return r;
304 if (r == 0) { /* Child */
305 r = run_editor_child(context);
306 _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
307 }
308
309 return 0;
310 }
311
312 static int strip_edit_temp_file(EditFile *e) {
313 _cleanup_free_ char *old_contents = NULL, *tmp = NULL, *new_contents = NULL;
314 const char *stripped;
315 int r;
316
317 assert(e);
318 assert(e->context);
319 assert(e->temp);
320
321 r = read_full_file(e->temp, &old_contents, NULL);
322 if (r < 0)
323 return log_error_errno(r, "Failed to read temporary file '%s': %m", e->temp);
324
325 tmp = strdup(old_contents);
326 if (!tmp)
327 return log_oom();
328
329 if (e->context->marker_start && !e->context->stdin) {
330 /* Trim out the lines between the two markers */
331 char *contents_start, *contents_end;
332
333 assert(e->context->marker_end);
334
335 contents_start = strstrafter(tmp, e->context->marker_start) ?: tmp;
336
337 contents_end = strstr(contents_start, e->context->marker_end);
338 if (contents_end)
339 *contents_end = '\0';
340
341 stripped = strstrip(contents_start);
342 } else
343 stripped = strstrip(tmp);
344
345 if (isempty(stripped)) {
346 /* File is empty (has no real changes) */
347 log_notice("%s: after editing, new contents are empty, not writing file.", e->path);
348 return 0;
349 }
350
351 /* Trim prefix and suffix, but ensure suffixed by single newline */
352 new_contents = strjoin(stripped, "\n");
353 if (!new_contents)
354 return log_oom();
355
356 if (streq(old_contents, new_contents)) /* Don't touch the file if the above didn't change a thing */
357 return 1; /* Contents have real changes */
358
359 r = write_string_file(e->temp, new_contents,
360 WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
361 if (r < 0)
362 return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp);
363
364 return 1; /* Contents have real changes */
365 }
366
367 int do_edit_files_and_install(EditFileContext *context) {
368 _cleanup_free_ char *data = NULL;
369 size_t data_size = 0;
370 int r;
371
372 assert(context);
373
374 if (context->n_files == 0)
375 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
376
377 if (context->stdin) {
378 r = read_full_stream(stdin, &data, &data_size);
379 if (r < 0)
380 return log_error_errno(r, "Failed to read stdin: %m");
381 }
382
383 FOREACH_ARRAY(editfile, context->files, context->n_files) {
384 r = create_edit_temp_file(editfile, data, data_size);
385 if (r < 0)
386 return r;
387 }
388
389 if (!context->stdin) {
390 r = run_editor(context);
391 if (r < 0)
392 return r;
393 }
394
395 FOREACH_ARRAY(editfile, context->files, context->n_files) {
396 /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */
397 r = strip_edit_temp_file(editfile);
398 if (r < 0)
399 return r;
400 if (r == 0) /* temp file doesn't carry actual changes, ignoring */
401 continue;
402
403 r = RET_NERRNO(rename(editfile->temp, editfile->path));
404 if (r < 0)
405 return log_error_errno(r,
406 "Failed to rename temporary file '%s' to target file '%s': %m",
407 editfile->temp,
408 editfile->path);
409 editfile->temp = mfree(editfile->temp);
410
411 log_info("Successfully installed edited file '%s'.", editfile->path);
412 }
413
414 return 0;
415 }