]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/bootspec.c
bootctl: show unique titles
[thirdparty/systemd.git] / src / shared / bootspec.c
1 /***
2 This file is part of systemd.
3
4 Copyright 2017 Zbigniew Jędrzejewski-Szmek
5
6 systemd is free software; you can redistribute it and/or modify it
7 under the terms of the GNU Lesser General Public License as published by
8 the Free Software Foundation; either version 2.1 of the License, or
9 (at your option) any later version.
10
11 systemd is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public License
17 along with systemd; If not, see <http://www.gnu.org/licenses/>.
18 ***/
19
20 #include <stdio.h>
21
22 #include "alloc-util.h"
23 #include "bootspec.h"
24 #include "conf-files.h"
25 #include "def.h"
26 #include "efivars.h"
27 #include "fd-util.h"
28 #include "fileio.h"
29 #include "string-util.h"
30 #include "strv.h"
31
32 void boot_entry_free(BootEntry *entry) {
33 free(entry->filename);
34
35 free(entry->title);
36 free(entry->show_title);
37 free(entry->version);
38 free(entry->machine_id);
39 free(entry->architecture);
40 strv_free(entry->options);
41 free(entry->kernel);
42 free(entry->efi);
43 strv_free(entry->initrd);
44 free(entry->device_tree);
45 }
46
47 int boot_entry_load(const char *path, BootEntry *entry) {
48 _cleanup_fclose_ FILE *f = NULL;
49 unsigned line = 1;
50 _cleanup_(boot_entry_free) BootEntry tmp = {};
51 int r;
52
53 f = fopen(path, "re");
54 if (!f)
55 return log_error_errno(errno, "Failed to open \"%s\": %m", path);
56
57 tmp.filename = strdup(basename(path));
58 if (!tmp.filename)
59 return log_oom();
60
61 for (;;) {
62 _cleanup_free_ char *buf = NULL;
63 char *p;
64
65 r = read_line(f, LONG_LINE_MAX, &buf);
66 if (r == 0)
67 break;
68 if (r == -ENOBUFS)
69 return log_error_errno(r, "%s:%u: Line too long", path, line);
70 if (r < 0)
71 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
72
73 line++;
74
75 if (IN_SET(*strstrip(buf), '#', '\0'))
76 continue;
77
78 p = strchr(buf, ' ');
79 if (!p) {
80 log_warning("%s:%u: Bad syntax", path, line);
81 continue;
82 }
83 *p = '\0';
84 p = strstrip(p + 1);
85
86 if (streq(buf, "title"))
87 r = free_and_strdup(&tmp.title, p);
88 else if (streq(buf, "version"))
89 r = free_and_strdup(&tmp.version, p);
90 else if (streq(buf, "machine-id"))
91 r = free_and_strdup(&tmp.machine_id, p);
92 else if (streq(buf, "architecture"))
93 r = free_and_strdup(&tmp.architecture, p);
94 else if (streq(buf, "options"))
95 r = strv_extend(&tmp.options, p);
96 else if (streq(buf, "linux"))
97 r = free_and_strdup(&tmp.kernel, p);
98 else if (streq(buf, "efi"))
99 r = free_and_strdup(&tmp.efi, p);
100 else if (streq(buf, "initrd"))
101 r = strv_extend(&tmp.initrd, p);
102 else if (streq(buf, "devicetree"))
103 r = free_and_strdup(&tmp.device_tree, p);
104 else {
105 log_notice("%s:%u: Unknown line \"%s\"", path, line, buf);
106 continue;
107 }
108 if (r < 0)
109 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
110 }
111
112 *entry = tmp;
113 tmp = (BootEntry) {};
114 return 0;
115 }
116
117 void boot_config_free(BootConfig *config) {
118 unsigned i;
119
120 free(config->default_pattern);
121 free(config->timeout);
122 free(config->editor);
123
124 free(config->entry_oneshot);
125 free(config->entry_default);
126
127 for (i = 0; i < config->n_entries; i++)
128 boot_entry_free(config->entries + i);
129 free(config->entries);
130 }
131
132 int boot_loader_read_conf(const char *path, BootConfig *config) {
133 _cleanup_fclose_ FILE *f = NULL;
134 unsigned line = 1;
135 int r;
136
137 f = fopen(path, "re");
138 if (!f)
139 return log_error_errno(errno, "Failed to open \"%s\": %m", path);
140
141 for (;;) {
142 _cleanup_free_ char *buf = NULL;
143 char *p;
144
145 r = read_line(f, LONG_LINE_MAX, &buf);
146 if (r == 0)
147 break;
148 if (r == -ENOBUFS)
149 return log_error_errno(r, "%s:%u: Line too long", path, line);
150 if (r < 0)
151 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
152
153 line++;
154
155 if (IN_SET(*strstrip(buf), '#', '\0'))
156 continue;
157
158 p = strchr(buf, ' ');
159 if (!p) {
160 log_warning("%s:%u: Bad syntax", path, line);
161 continue;
162 }
163 *p = '\0';
164 p = strstrip(p + 1);
165
166 if (streq(buf, "default"))
167 r = free_and_strdup(&config->default_pattern, p);
168 else if (streq(buf, "timeout"))
169 r = free_and_strdup(&config->timeout, p);
170 else if (streq(buf, "editor"))
171 r = free_and_strdup(&config->editor, p);
172 else {
173 log_notice("%s:%u: Unknown line \"%s\"", path, line, buf);
174 continue;
175 }
176 if (r < 0)
177 return log_error_errno(r, "%s:%u: Error while reading: %m", path, line);
178 }
179
180 return 0;
181 }
182
183 /* This is a direct translation of str_verscmp from boot.c */
184 static bool is_digit(int c) {
185 return c >= '0' && c <= '9';
186 }
187
188 static int c_order(int c) {
189 if (c == '\0')
190 return 0;
191 if (is_digit(c))
192 return 0;
193 else if ((c >= 'a') && (c <= 'z'))
194 return c;
195 else
196 return c + 0x10000;
197 }
198
199 static int str_verscmp(const char *s1, const char *s2) {
200 const char *os1 = s1;
201 const char *os2 = s2;
202
203 while (*s1 || *s2) {
204 int first;
205
206 while ((*s1 && !is_digit(*s1)) || (*s2 && !is_digit(*s2))) {
207 int order;
208
209 order = c_order(*s1) - c_order(*s2);
210 if (order)
211 return order;
212 s1++;
213 s2++;
214 }
215
216 while (*s1 == '0')
217 s1++;
218 while (*s2 == '0')
219 s2++;
220
221 first = 0;
222 while (is_digit(*s1) && is_digit(*s2)) {
223 if (first == 0)
224 first = *s1 - *s2;
225 s1++;
226 s2++;
227 }
228
229 if (is_digit(*s1))
230 return 1;
231 if (is_digit(*s2))
232 return -1;
233
234 if (first != 0)
235 return first;
236 }
237
238 return strcmp(os1, os2);
239 }
240
241 static int boot_entry_compare(const void *a, const void *b) {
242 const BootEntry *aa = a;
243 const BootEntry *bb = b;
244
245 return str_verscmp(aa->filename, bb->filename);
246 }
247
248 int boot_entries_find(const char *dir, BootEntry **entries, size_t *n_entries) {
249 _cleanup_strv_free_ char **files = NULL;
250 char **f;
251 int r;
252
253 BootEntry *array = NULL;
254 size_t n_allocated = 0, n = 0;
255
256 r = conf_files_list(&files, ".conf", NULL, 0, dir, NULL);
257 if (r < 0)
258 return log_error_errno(r, "Failed to list files in \"%s\": %m", dir);
259
260 STRV_FOREACH(f, files) {
261 if (!GREEDY_REALLOC0(array, n_allocated, n + 1))
262 return log_oom();
263
264 r = boot_entry_load(*f, array + n);
265 if (r < 0)
266 continue;
267
268 n++;
269 }
270
271 qsort_safe(array, n, sizeof(BootEntry), boot_entry_compare);
272
273 *entries = array;
274 *n_entries = n;
275 return 0;
276 }
277
278 static bool find_nonunique(BootEntry *entries, size_t n_entries, bool *arr) {
279 unsigned i, j;
280 bool non_unique = false;
281
282 for (i = 0; i < n_entries; i++)
283 arr[i] = false;
284
285 for (i = 0; i < n_entries; i++)
286 for (j = 0; j < n_entries; j++)
287 if (i != j && streq(boot_entry_title(entries + i),
288 boot_entry_title(entries + j)))
289 non_unique = arr[i] = arr[j] = true;
290
291 return non_unique;
292 }
293
294 static int boot_entries_uniquify(BootEntry *entries, size_t n_entries) {
295 char *s;
296 unsigned i;
297 int r;
298 bool arr[n_entries];
299
300 /* Find _all_ non-unique titles */
301 if (!find_nonunique(entries, n_entries, arr))
302 return 0;
303
304 /* Add version to non-unique titles */
305 for (i = 0; i < n_entries; i++)
306 if (arr[i] && entries[i].version) {
307 r = asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].version);
308 if (r < 0)
309 return -ENOMEM;
310
311 free_and_replace(entries[i].show_title, s);
312 }
313
314 if (!find_nonunique(entries, n_entries, arr))
315 return 0;
316
317 /* Add machine-id to non-unique titles */
318 for (i = 0; i < n_entries; i++)
319 if (arr[i] && entries[i].machine_id) {
320 r = asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].machine_id);
321 if (r < 0)
322 return -ENOMEM;
323
324 free_and_replace(entries[i].show_title, s);
325 }
326
327 if (!find_nonunique(entries, n_entries, arr))
328 return 0;
329
330 /* Add file name to non-unique titles */
331 for (i = 0; i < n_entries; i++)
332 if (arr[i]) {
333 r = asprintf(&s, "%s (%s)", boot_entry_title(entries + i), entries[i].filename);
334 if (r < 0)
335 return -ENOMEM;
336
337 free_and_replace(entries[i].show_title, s);
338 }
339
340 return 0;
341 }
342
343 int boot_entries_select_default(const BootConfig *config) {
344 int i;
345
346 if (config->entry_oneshot)
347 for (i = config->n_entries - 1; i >= 0; i--)
348 if (streq(config->entry_oneshot, config->entries[i].filename)) {
349 log_debug("Found default: filename \"%s\" is matched by LoaderEntryOneShot",
350 config->entries[i].filename);
351 return i;
352 }
353
354 if (config->entry_default)
355 for (i = config->n_entries - 1; i >= 0; i--)
356 if (streq(config->entry_default, config->entries[i].filename)) {
357 log_debug("Found default: filename \"%s\" is matched by LoaderEntryDefault",
358 config->entries[i].filename);
359 return i;
360 }
361
362 if (config->default_pattern)
363 for (i = config->n_entries - 1; i >= 0; i--)
364 if (fnmatch(config->default_pattern, config->entries[i].filename, FNM_CASEFOLD) == 0) {
365 log_debug("Found default: filename \"%s\" is matched by pattern \"%s\"",
366 config->entries[i].filename, config->default_pattern);
367 return i;
368 }
369
370 if (config->n_entries > 0)
371 log_debug("Found default: last entry \"%s\"", config->entries[i].filename);
372 else
373 log_debug("Found no default boot entry :(");
374 return config->n_entries - 1; /* -1 means "no default" */
375 }
376
377 int boot_entries_load_config(const char *esp_path, BootConfig *config) {
378 const char *p;
379 int r;
380
381 p = strjoina(esp_path, "/loader/loader.conf");
382 r = boot_loader_read_conf(p, config);
383 if (r < 0)
384 return log_error_errno(r, "Failed to read boot config from \"%s\": %m", p);
385
386 p = strjoina(esp_path, "/loader/entries");
387 r = boot_entries_find(p, &config->entries, &config->n_entries);
388 if (r < 0)
389 return log_error_errno(r, "Failed to read boot entries from \"%s\": %m", p);
390
391 r = boot_entries_uniquify(config->entries, config->n_entries);
392 if (r < 0)
393 return log_error_errno(r, "Failed to uniquify boot entries: %m");
394
395 r = efi_get_variable_string(EFI_VENDOR_LOADER, "LoaderEntryOneShot", &config->entry_oneshot);
396 if (r < 0 && r != -ENOENT)
397 return log_error_errno(r, "Failed to read EFI var \"LoaderEntryOneShot\": %m");
398
399 r = efi_get_variable_string(EFI_VENDOR_LOADER, "LoaderEntryDefault", &config->entry_default);
400 if (r < 0 && r != -ENOENT)
401 return log_error_errno(r, "Failed to read EFI var \"LoaderEntryDefault\": %m");
402
403 config->default_entry = boot_entries_select_default(config);
404 return 0;
405 }