discover/syslinux-parser: filter out duplicate conf files
[petitboot] / discover / syslinux-parser.c
1 #if defined(HAVE_CONFIG_H)
2 #include "config.h"
3 #endif
4
5 #include <assert.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <i18n/i18n.h>
9 #include <libgen.h>
10
11 #include "log/log.h"
12 #include "list/list.h"
13 #include "file/file.h"
14 #include "talloc/talloc.h"
15 #include "types/types.h"
16 #include "parser-conf.h"
17 #include "parser-utils.h"
18 #include "resource.h"
19 #include "paths.h"
20
21 struct syslinux_boot_option {
22         char *label;
23         char *image;
24         char *append;
25         char *initrd;
26         struct list_item list;
27 };
28
29 /* by spec 16 is allowed */
30 #define INCLUDE_NEST_LIMIT 16
31
32 struct syslinux_options {
33         struct list processed_options;
34         struct syslinux_boot_option *current_option;
35         int include_nest_level;
36         char *cfg_dir;
37 };
38
39 struct conf_file_stat {
40         char *name;
41         struct stat stat;
42         struct list_item list;
43 };
44
45 static const char *const syslinux_conf_files[] = {
46         "/boot/syslinux/syslinux.cfg",
47         "/syslinux/syslinux.cfg",
48         "/syslinux.cfg",
49         "/BOOT/SYSLINUX/SYSLINUX.CFG",
50         "/SYSLINUX/SYSLINUX.CFG",
51         "/SYSLINUX.CFG",
52         NULL
53 };
54
55 static const char *const syslinux_kernel_unsupported_extensions[] = {
56         ".0", /* eventually support PXE here? */
57         ".bin",
58         ".bs",
59         ".bss",
60         ".c32",
61         ".cbt",
62         ".com",
63         ".img",
64         NULL
65 };
66
67 static const char *const syslinux_ignored_names[] = {
68         "config",
69         "sysapend",
70         "localboot",
71         "ui",
72         "prompt",
73         "noescape",
74         "nocomplete",
75         "allowoptions",
76         "timeout",
77         "totaltimeout",
78         "ontimeout",
79         "onerror",
80         "serial",
81         "nohalt",
82         "console",
83         "font",
84         "kbdmap",
85         "say",
86         "display",
87         "f1",
88         "f2",
89         "f3",
90         "f4",
91         "f5",
92         "f6",
93         "f7",
94         "f8",
95         "f9",
96         "f10",
97         "f11",
98         "f12",
99         NULL
100 };
101
102 static const char *const syslinux_unsupported_boot_names[] = {
103         "boot",
104         "bss",
105         "pxe",
106         "fdimage",
107         "comboot",
108         "com32",
109         NULL
110 };
111
112 static struct conf_global_option syslinux_global_options[] = {
113         { .name = "default" },
114         { .name = "implicit" },
115         { .name = "append" },
116         { .name = NULL }
117 };
118
119
120 static void finish_boot_option(struct syslinux_options *state,
121                                bool free_if_unused)
122 {
123         /*
124          * in the normal this function signals a new image block which means
125          * move the current block to the list of processed items
126          * the special case is a label before an image block which we need to
127          * know whether to keep it for further processing or junk it
128          */
129         if (state->current_option) {
130                 if (state->current_option->image) {
131                         list_add(&state->processed_options,
132                                  &state->current_option->list);
133                         state->current_option = NULL;
134                 } else if (free_if_unused) {
135                         talloc_free(state->current_option);
136                         state->current_option = NULL;
137                 }
138         }
139 }
140
141 static bool start_new_option(struct syslinux_options *state)
142 {
143         bool ret = false;
144
145         finish_boot_option(state, false);
146         if (!state->current_option)
147                 state->current_option = talloc_zero(state, struct syslinux_boot_option);
148
149         if (state->current_option)
150                 ret = true;
151
152         return ret;
153 }
154
155 static void syslinux_process_pair(struct conf_context *conf, const char *name, char *value)
156 {
157         struct syslinux_options *state = conf->parser_info;
158         char *buf, *pos, *path;
159         int len, rc;
160
161         /* ignore bare values */
162         if (!name)
163                 return;
164
165         if (conf_param_in_list(syslinux_ignored_names, name))
166                 return;
167
168         /* a new boot entry needs to terminate any prior one */
169         if (conf_param_in_list(syslinux_unsupported_boot_names, name)) {
170                 finish_boot_option(state, true);
171                 return;
172         }
173
174         if (streq(name, "label")) {
175                 finish_boot_option(state, true);
176                 state->current_option = talloc_zero(state,
177                                             struct syslinux_boot_option);
178                 if (state->current_option)
179                         state->current_option->label = talloc_strdup(state, value);
180                 return;
181         }
182
183         if (streq(name, "linux")) {
184
185                 if (start_new_option(state)) {
186                         state->current_option->image = talloc_strdup(state, value);
187                         if (!state->current_option->image) {
188                                 talloc_free(state->current_option);
189                                 state->current_option = NULL;
190                         }
191                 }
192
193                 return;
194         }
195
196         if (streq(name, "kernel")) {
197
198                 if (start_new_option(state)) {
199                 /*
200                  * by spec a linux image can not have any of these
201                  * extensions, it can have no extension or anything not
202                  * in this list
203                  */
204                         pos = strrchr(value, '.');
205                         if (!pos ||
206                         !conf_param_in_list(syslinux_kernel_unsupported_extensions, pos)) {
207                                 state->current_option->image = talloc_strdup(state, value);
208                                 if (!state->current_option->image) {
209                                         talloc_free(state->current_option);
210                                         state->current_option = NULL;
211                                 }
212                         } else  /* clean up any possible trailing label */
213                                 finish_boot_option(state, true);
214                 }
215                 return;
216         }
217
218
219
220         /* APPEND can be global and/or local so need special handling */
221         if (streq(name, "append")) {
222                 if (state->current_option) {
223                         /* by spec only take last if multiple APPENDs */
224                         if (state->current_option->append)
225                                 talloc_free(state->current_option->append);
226                         state->current_option->append = talloc_strdup(state, value);
227                         if (!state->current_option->append) {
228                                 talloc_free(state->current_option);
229                                 state->current_option = NULL;
230                         }
231                 } else {
232                         finish_boot_option(state, true);
233                         conf_set_global_option(conf, name, value);
234                 }
235                 return;
236         }
237
238         /* now the general globals */
239         if (conf_set_global_option(conf, name, value)) {
240                 finish_boot_option(state, true);
241                 return;
242         }
243
244         if (streq(name, "initrd")) {
245                 if (state->current_option) {
246                         state->current_option->initrd = talloc_strdup(state, value);
247                         if (!state->current_option->initrd) {
248                                 talloc_free(state->current_option);
249                                 state->current_option = NULL;
250                         }
251                 }
252                 return;
253         }
254
255         if (streq(name, "include")) {
256                 if (state->include_nest_level < INCLUDE_NEST_LIMIT) {
257                         state->include_nest_level++;
258
259                         /* if absolute in as-is */
260                         if (value[0] == '/')
261                                 path = talloc_strdup(state, value);
262                         else /* otherwise relative to the root config file */
263                                 path = join_paths(state, state->cfg_dir, value);
264
265                         rc = parser_request_file(conf->dc, conf->dc->device, path, &buf, &len);
266                         if (!rc) {
267                                 conf_parse_buf(conf, buf, len);
268
269                                 device_handler_status_dev_info(conf->dc->handler, conf->dc->device,
270                                 _("Parsed nested syslinux configuration from %s"), value);
271                                 talloc_free(buf);
272                         } else {
273                                 device_handler_status_dev_info(conf->dc->handler, conf->dc->device,
274                                 _("Failed to parse nested syslinux configuration from %s"), value);
275                         }
276
277                         talloc_free(path);
278
279                         state->include_nest_level--;
280                 } else {
281                         device_handler_status_dev_err(conf->dc->handler, conf->dc->device,
282                                 _("Nested syslinux INCLUDE exceeds limit...ignored"));
283                 }
284                 return;
285         }
286
287         pb_debug("%s: unknown name: %s\n", __func__, name);
288 }
289
290 static void syslinux_finalize(struct conf_context *conf)
291 {
292         struct syslinux_options *state = conf->parser_info;
293         struct syslinux_boot_option *syslinux_opt, *tmp;
294         struct discover_context *dc = conf->dc;
295         struct discover_boot_option *d_opt;
296         bool implicit_image = true;
297         char *args_sigfile_default;
298         const char *global_default;
299         const char *global_append;
300         struct boot_option *opt;
301         const char *image;
302         const char *label;
303
304         /* clean up any lingering boot entries */
305         finish_boot_option(state, true);
306
307         global_append  = conf_get_global_option(conf, "append");
308         global_default = conf_get_global_option(conf, "default");
309
310         /*
311          * by spec '0' means disable
312          * note we set the default to '1' (which is by spec) in syslinux_parse
313          */
314         if (conf_get_global_option(conf, "implicit"), "0")
315                 implicit_image = false;
316
317         list_for_each_entry(&state->processed_options, syslinux_opt, list) {
318                 /* need a valid image */
319                 if (!syslinux_opt->image)
320                         continue;
321
322                 image = syslinux_opt->image;
323                 label = syslinux_opt->label;
324
325                 /* if implicit is disabled we must have a label */
326                 if (!label && !implicit_image)
327                         continue;
328
329                 d_opt = discover_boot_option_create(dc, dc->device);
330                 if (!d_opt)
331                         continue;
332                 if (!d_opt->option)
333                         goto fail;
334
335                 opt = d_opt->option;
336
337                 if (syslinux_opt->append) {
338                         /* '-' can signal do not use global APPEND */
339                         if (!strcmp(syslinux_opt->append, "-"))
340                                 opt->boot_args = talloc_strdup(opt, "");
341                         else
342                                 opt->boot_args = talloc_asprintf(opt, "%s %s",
343                                                                  global_append,
344                                                                  syslinux_opt->append);
345                 } else
346                         opt->boot_args = talloc_strdup(opt, global_append);
347
348                 if (!opt->boot_args)
349                         goto fail;
350
351                 opt->id = talloc_asprintf(opt, "%s#%s", dc->device->device->id, image);
352
353                 if (!opt->id)
354                         goto fail;
355
356                 if (label) {
357                         opt->name = talloc_strdup(opt, label);
358                         if (!strcmp(label, global_default))
359                                 opt->is_default = true;
360
361                         opt->description = talloc_asprintf(opt, "(%s) %s", label, image);
362                 } else {
363                         opt->name = talloc_strdup(opt, image);
364                         opt->description = talloc_strdup(opt, image);
365                 }
366
367                 if (!opt->name)
368                         goto fail;
369
370                 d_opt->boot_image = create_devpath_resource(d_opt, dc->device, image);
371
372                 if(!d_opt->boot_image)
373                         goto fail;
374
375                 if (syslinux_opt->initrd) {
376                         d_opt->initrd = create_devpath_resource(d_opt, dc->device,
377                                                                 syslinux_opt->initrd);
378                         opt->description = talloc_asprintf_append(opt->description, 
379                                                                   " initrd=%s",
380                                                                   syslinux_opt->initrd);
381
382                         if (!d_opt->initrd)
383                                 goto fail;
384                 }
385
386                 opt->description = talloc_asprintf_append(opt->description,
387                                                           " args=%s",
388                                                           opt->boot_args);
389
390                 if (!opt->description)
391                         goto fail;
392
393                 args_sigfile_default = talloc_asprintf(d_opt, "%s.cmdline.sig",
394                                                        image);
395
396                 if (!args_sigfile_default)
397                         goto fail;
398
399                 d_opt->args_sig_file = create_devpath_resource(d_opt, dc->device,
400                                                                args_sigfile_default);
401
402                 if (!d_opt->args_sig_file)
403                         goto fail;
404
405                 talloc_free(args_sigfile_default);
406
407                 conf_strip_str(opt->boot_args);
408                 conf_strip_str(opt->description);
409
410                 discover_context_add_boot_option(dc, d_opt);
411                 continue;
412
413 fail:
414                 talloc_free(d_opt);
415         }
416
417         list_for_each_entry_safe(&state->processed_options, syslinux_opt, tmp, list)
418                 talloc_free(syslinux_opt);
419         list_init(&state->processed_options);
420 }
421
422 static int syslinux_parse(struct discover_context *dc)
423 {
424         struct conf_file_stat *confcmp, *confdat;
425         struct list processed_conf_files;
426         struct syslinux_options *state;
427         const char * const *filename;
428         struct conf_context *conf;
429         struct stat statbuf;
430         char *cfg_dir;
431         int len, rc;
432         char *buf;
433
434         /* Support block device boot only at present */
435         if (dc->event)
436                 return -1;
437
438         conf = talloc_zero(dc, struct conf_context);
439
440         if (!conf)
441                 return -1;
442
443         conf->dc = dc;
444         conf->global_options = syslinux_global_options,
445         conf_init_global_options(conf);
446         conf->get_pair = conf_get_pair_space;
447         conf->process_pair = syslinux_process_pair;
448
449         conf->parser_info = state = talloc_zero(conf, struct syslinux_options);
450         list_init(&state->processed_options);
451
452         list_init(&processed_conf_files);
453
454         /*
455          * set the global defaults
456          * by spec 'default' defaults to 'linux' and
457          * 'implicit' defaults to '1', we also just set
458          * and empty string in 'append' to make it easier
459          * in syslinux_finish
460          */
461         conf_set_global_option(conf, "default", "linux");
462         conf_set_global_option(conf, "implicit", "1");
463         conf_set_global_option(conf, "append", "");
464
465         for (filename = syslinux_conf_files; *filename; filename++) {
466                 /*
467                  * guard against duplicate entries in case-insensitive
468                  * filesystems, mainly vfat boot partitions
469                  */
470                 rc = parser_stat_path(dc, dc->device, *filename, &statbuf);
471                 if (rc)
472                         continue;
473
474                 rc = 0;
475
476                 list_for_each_entry(&processed_conf_files, confcmp, list) {
477                         if (confcmp->stat.st_ino == statbuf.st_ino) {
478                                 pb_log("conf file %s is a path duplicate of %s..skipping\n",
479                                        *filename, confcmp->name);
480                                 rc = 1;
481                                 break;
482                         }
483                 }
484
485                 if (rc)
486                         continue;
487
488                 rc = parser_request_file(dc, dc->device, *filename, &buf, &len);
489                 if (rc)
490                         continue;
491
492                 confdat = talloc_zero(conf, struct conf_file_stat);
493                 confdat->stat = statbuf;
494                 confdat->name = talloc_strdup(confdat, *filename);
495                 list_add(&processed_conf_files, &confdat->list);
496
497                 /*
498                  * save location of root config file for possible
499                  * INCLUDE directives later
500                  *
501                  * dirname can overwrite so need local copy to work on
502                  */
503                 cfg_dir = talloc_strdup(conf, *filename);
504                 state->cfg_dir = talloc_strdup(state, dirname(cfg_dir));
505                 talloc_free(cfg_dir);
506
507                 conf_parse_buf(conf, buf, len);
508                 device_handler_status_dev_info(dc->handler, dc->device,
509                                 _("Parsed syslinux configuration from %s"),
510                                 *filename);
511                 talloc_free(buf);
512
513                 syslinux_finalize(conf);
514         }
515
516         talloc_free(conf);
517         return 0;
518 }
519
520 static struct parser syslinux_parser = {
521         .name                   = "syslinux",
522         .parse                  = syslinux_parse,
523         .resolve_resource       = resolve_devpath_resource,
524 };
525
526 register_parser(syslinux_parser);