/* * SPDX-FileCopyrightText: 2024 fosslinux * * SPDX-License-Identifier: GPL-3.0-or-later */ #define MAX_STRING 2048 #define MAX_SHORT 512 #define MAX_ID 128 #define MAX_VAR 128 #include #include #include #include #include #include #define TRUE 1 #define FALSE 0 #define KIND_NONE 0 #define KIND_MENU 1 #define KIND_OPTION 2 #define TYPE_NONE 0 #define TYPE_BOOL 1 #define TYPE_SIZE 2 #define TYPE_STRING 3 #define TYPE_INT 4 struct Entry { int kind; // either menu or option char *env_var; // name of the environment variable this option is stored in char *id; // the id of the configuration item char *short_desc; // short description of the config char *full_desc; // extended description of the config int type; // the type of the configuration option char *validation; // any validation rules char *val; char *default_val; struct Entry *children; // submenus struct Entry *parent; struct Entry *next; }; typedef struct Entry Entry; Entry *find_entry(Entry *head, char *id) { char *component = strchr(id, '/'); if (component == NULL) { component = id + strlen(id); } Entry *current; Entry *final; int len; while (1) { len = component - id; current = head; while (current != NULL) { /* ensure that the id isn't just a substring of the component but actually is the component */ if (strlen(current->id) == len && strncmp(id, current->id, len) == 0) { /* Found it! */ final = current; head = current->children; break; } current = current->next; } if (current == NULL) { /* Did not find it */ return NULL; } if (component[0] == '\0') { break; } component += 1; component = strchr(component, '/'); if (component == NULL) { component = id + strlen(id); } } return final; } Entry *get_parent(Entry *head, char *id) { char *parent_id = calloc(MAX_ID, sizeof(char)); strcpy(parent_id, id); char *final_slash = strrchr(parent_id, '/'); final_slash[0] = '\0'; Entry *ret = find_entry(head, parent_id); free(parent_id); return ret; } char read_string(FILE *f, char *out, int length) { int i = 0; char c = fgetc(f); while (c != ' ' && c != '\n' && c != EOF && i < length - 1) { out[i] = c; i += 1; c = fgetc(f); } if (i >= length - 1) { fputs("String too long!\n", stdout); fclose(f); exit(1); } out[i] = '\0'; return c; } int set_val(Entry *entry, char *val) { if (entry->type == TYPE_BOOL) { if (strcmp(val, "True") != 0 && strcmp(val, "False") != 0) { fputs("Invalid input: ", stdout); fputs(val, stdout); fputs(" is not a boolean value\n", stdout); return 1; } } else if (entry->type == TYPE_INT) { int intval = strtoint(val); if (intval == 0 && strcmp(val, "0") != 0) { fputs("Invalid input: ", stdout); fputs(val, stdout); fputs(" is not an integer\n", stdout); return 1; } } else if (entry->type == TYPE_SIZE) { /* We should have either a K, M, G, T, or no letter, at the end of the size. */ char c = val[strlen(val) - 1]; if (!(('0' <= c && c <= '9') || c == 'K' || c == 'M' || c == 'G' || c == 'T')) { fputs("Invalid input: ", stdout); fputc(c, stdout); fputs(" is not a valid suffix for a size\n", stdout); return 1; } /* Check it is an integer */ char *final_char = val + strlen(val) - 1; if ('A' <= final_char[0] && final_char[0] <= 'Z') { final_char[0] = '\0'; } int intval = strtoint(val); if (intval == 0 && strcmp(val, "0") != 0) { fputs("Invalid input: ", stdout); fputs(val, stdout); fputs(" is not a valid size\n", stdout); return 1; } final_char[0] = c; } else if (entry->type == TYPE_STRING) { /* Validation rules. */ char *validation = entry->validation; char *next; int found = FALSE; while (validation != NULL) { if (validation[0] == '\0') { found = TRUE; break; } next = strchr(validation, '|'); if (next == NULL) { if (strcmp(validation, val) == 0) { found = TRUE; } break; } else { if (strncmp(validation, val, next - validation) == 0) { found = TRUE; } } validation = next + 1; } if (found == FALSE) { fputs("Invalid input: ", stdout); fputs(val, stdout); fputs(" does not match the validation rules ", stdout); fputs(entry->validation, stdout); fputc('\n', stdout); return 1; } } entry->val = calloc(strlen(val) + 1, sizeof(char)); strcpy(entry->val, val); return 0; } void read_entry(char c, FILE *conf, Entry *head) { Entry *new = calloc(1, sizeof(Entry)); /* Read the kind */ if (c == 'm') { new->kind = KIND_MENU; } else if (c == 'o') { new->kind = KIND_OPTION; } else { fputs("Invalid entry: kind ", stdout); fputc(c, stdout); fputc('\n', stdout); fclose(conf); exit(1); } fgetc(conf); /* Read the id */ new->id = calloc(MAX_ID, sizeof(char)); c = read_string(conf, new->id, MAX_ID); if (c != ' ') { fputs("Invalid entry: no variable\n", stdout); fclose(conf); exit(1); } /* Read the environment variable */ new->env_var = calloc(MAX_VAR, sizeof(char)); c = read_string(conf, new->env_var, MAX_VAR); if (c != ' ') { fputs("Invalid entry: no data type\n", stdout); fclose(conf); exit(1); } if (strcmp(new->env_var, "_") == 0) { free(new->env_var); new->env_var = NULL; } /* Read the data type */ char *data_type = calloc(MAX_ID, sizeof(char)); read_string(conf, data_type, MAX_ID); if (c != ' ') { fputs("Invalid entry: no default value\n", stdout); fclose(conf); exit(1); } if (strcmp(data_type, "_") == 0) { new->type = TYPE_NONE; } else if (strcmp(data_type, "bool") == 0) { new->type = TYPE_BOOL; } else if (strcmp(data_type, "size") == 0) { new->type = TYPE_SIZE; } else if (strcmp(data_type, "int") == 0) { new->type = TYPE_INT; } else if (data_type[0] == '"') { new->type = TYPE_STRING; new->validation = data_type + 1; char *closing_quote = strrchr(data_type, '"'); closing_quote[0] = '\0'; } else { fputs("Invalid entry: unknown type: ", stdout); fputs(data_type, stdout); fputc('\n', stdout); fclose(conf); exit(1); } if (new->type != TYPE_STRING) { free(data_type); } /* Read the default value */ char *default_val = calloc(MAX_STRING, sizeof(char)); read_string(conf, default_val, MAX_ID); if (strcmp(default_val, "_") != 0) { set_val(new, default_val); new->default_val = default_val; } else { new->default_val = NULL; } /* Read the short description */ new->short_desc = calloc(MAX_SHORT, sizeof(char)); int i = 0; c = fgetc(conf); while (c != '\n' && c != EOF) { new->short_desc[i] = c; c = fgetc(conf); i += 1; } /* Read the long description */ new->full_desc = calloc(MAX_STRING, sizeof(char)); i = 0; c = fgetc(conf); char prev = '\0'; while (!(c == '\n' && prev == '\n') && c != EOF) { new->full_desc[i] = c; prev = c; c = fgetc(conf); i += 1; } new->children = NULL; new->next = NULL; Entry *parent = get_parent(head, new->id); new->parent = parent; if (parent->children == NULL) { parent->children = new; } else { Entry *current = parent->children; while (current->next != NULL) { current = current->next; } current->next = new; } } Entry *read_config(char *filename) { FILE *conf = fopen(filename, "r"); if (conf == NULL) { fputs("Unable to open ", stdout); fputs(filename, stdout); fputc('\n', stdout); exit(0); } char c = fgetc(conf); Entry *head = calloc(1, sizeof(Entry)); head->id = ""; head->env_var = ""; head->next = NULL; Entry *current = head; while (c != EOF) { if (c == '#' || c == '\n') { /* Skip comments or empty lines. */ while (c != '\n' && c != EOF) { c = fgetc(conf); } } else { read_entry(c, conf, head); } c = fgetc(conf); } fclose(conf); return head; } Entry *get_env_var(Entry *head, char *var) { Entry *ret; Entry *current; for (current = head->children; current != NULL; current = current->next) { if (current->env_var != NULL) { if (strcmp(current->env_var, var) == 0) { return current; } } if (current->children != NULL) { ret = get_env_var(current, var); if (ret != NULL) { return ret; } } } return NULL; } int set_cfg_varline(Entry *head, char *line) { char *var = calloc(strlen(line) + 1, sizeof(char)); strcpy(var, line); char *val = strchr(var, '='); val[0] = '\0'; val += 1; char *newline = strchr(val, '\n'); if (newline != NULL) { newline[0] = '\0'; } Entry *entry = get_env_var(head, var); if (entry != NULL) { int not_ok = set_val(entry, val); if (not_ok) { fputs("^ Originated from ", stdout); fputs(var, stdout); fputs("=", stdout); fputs(val, stdout); fputs("\n", stdout); } } return entry == NULL; } char *set_cfg_values(Entry *head, char **envp) { int i = 0; FILE *cfg = fopen("/steps/bootstrap.cfg", "r"); if (cfg == NULL) { return ""; } char *extra = calloc(MAX_STRING, sizeof(char)); char *line = calloc(MAX_STRING, sizeof(char)); while (fgets(line, MAX_STRING, cfg) != NULL) { if (set_cfg_varline(head, line)) { if (strncmp("CONFIGURATOR=", line, 13) != 0) { strcat(extra, line); } } free(line); line = calloc(MAX_STRING, sizeof(char)); } fclose(cfg); return extra; } void write_cfg_list(Entry *head, FILE *cfg) { Entry *current; for (current = head->children; current != NULL; current = current->next) { if (current->kind == KIND_OPTION && current->val != NULL) { fputs(current->env_var, cfg); fputs("=", cfg); fputs(current->val, cfg); fputs("\n", cfg); } if (current->children != NULL) { write_cfg_list(current, cfg); } } } void write_cfg_values(Entry *head, char *extra, int configurator_done) { FILE *cfg = fopen("/steps/bootstrap.cfg", "w"); if (cfg == NULL) { fputs("Unable to open /steps/bootstrap.cfg", stderr); exit(1); } if (configurator_done == TRUE) { fputs("CONFIGURATOR=False\n", cfg); } fputs(extra, cfg); write_cfg_list(head, cfg); fclose(cfg); } void print_short_desc(char *short_desc) { char *post_markers = strrchr(short_desc, ']'); if (post_markers == NULL) { post_markers = short_desc; } else { post_markers += 1; while (post_markers[0] == ' ') { post_markers += 1; } } fputs(post_markers, stdout); } void print_recursive_desc(Entry *entry) { if (entry->parent != NULL) { if (strcmp(entry->parent->id, "") != 0) { print_recursive_desc(entry->parent); fputs("/", stdout); print_short_desc(entry->short_desc); return; } } print_short_desc(entry->short_desc); } int any_unset(Entry *head); int check_set(Entry *entry, Entry *head) { int ret = 0; if (entry->kind == KIND_OPTION && entry->val == NULL) { fputs("The configuration option ", stdout); print_recursive_desc(entry); fputs(" is unset\n", stdout); ret = 1; } if (entry->children != NULL) { ret |= any_unset(entry); } return ret; } int any_unset(Entry *head) { int ret = 0; Entry *current; for (current = head->children; current != NULL; current = current->next) { ret |= check_set(current, head); } return ret; } void print_menu(Entry *menu, int is_toplevel) { if (!is_toplevel) { fputs("(0) [MENU] Go up\n", stdout); } int i = 1; Entry *current; for (current = menu->children; current != NULL; current = current->next) { fputs("(", stdout); fputs(int2str(i, 10, FALSE), stdout); fputs(") ", stdout); if (current->kind == KIND_MENU) { fputs("[MENU] ", stdout); } fputs(current->short_desc, stdout); fputc('\n', stdout); i += 1; } } Entry *get_nth_option(Entry *menu, int n) { int i = 1; Entry *current; for (current = menu->children; current != NULL && i < n; current = current->next) { i += 1; } if (current == NULL) { fputs("There is no option ", stdout); fputs(int2str(n, 10, FALSE), stdout); fputs("!\n", stdout); } return current; } void how_to_use(void) { fputs( "How to navigate around this configuration menu:\n" "h or help: at any time, will reprint this help message\n" "l or list: shows the current menu options\n" "o or open : open a (sub)menu\n" "? or describe : provides a more detailed description of an option or menu\n" "s or set : set the value of an option\n" "g or get : get the value of an option\n" "g all or get all: get the value of all options in the menu\n" "r or reset : reset the value of an option to the default (if there is one)\n" "e or exit: exits the program\n", stdout); } Entry *extract_num(char **command, Entry *menu) { command[0] = strchr(command[0], ' '); if (command[0] == NULL) { fputs("Expected menu number to operate on!\n", stdout); } command[0] += 1; char *num = command[0]; char *new = strchr(command[0], ' '); if (new == NULL) { new = strchr(command[0], '\n'); } command[0] = new; command[0][0] = '\0'; command[0] += 1; /* strtoint does not check if it is not a number */ int i; for (i = 0; i < strlen(num); i += 1) { if (!('0' <= num[i] && num[i] <= '9')) { fputs(num, stdout); fputs(" is not a menu number!\n", stdout); return NULL; } } int n = strtoint(num); return get_nth_option(menu, n); } Entry *submenu(char *command, Entry *menu, Entry *head) { command = strchr(command, ' '); if (strlen(command) < 1) { fputs("Expected menu number to operate on!\n", stdout); } /* 0 is the "go up" menu option */ if (command[1] == '0') { if (strcmp(menu->id, "") == 0) { fputs("There is no option 0!\n", stdout); return menu; } return menu->parent; } Entry *new = extract_num(&command, menu); if (new == NULL) { return menu; } if (new->kind != KIND_MENU) { fputs("This is not a menu!\n", stdout); return menu; } return new; } void print_description(char *command, Entry *menu) { Entry *opt = extract_num(&command, menu); if (opt != NULL) { fputs(opt->full_desc, stdout); } } void set_opt_value(char *command, Entry *menu) { Entry *opt = extract_num(&command, menu); if (opt == NULL) { return; } if (opt->kind != KIND_OPTION) { fputs("Cannot set a menu's value!\n", stdout); return; } /* Remove the newline */ char *newline = strchr(command, '\n'); newline[0] = '\0'; set_val(opt, command); } void print_opt_value(Entry *opt) { print_short_desc(opt->short_desc); fputs(": ", stdout); if (opt->val == NULL) { fputs("unset", stdout); } else { fputs(opt->val, stdout); } fputc('\n', stdout); } void get_opt_value(char *command, Entry *menu) { Entry *opt = extract_num(&command, menu); if (opt == NULL) { return; } if (opt->kind != KIND_OPTION) { fputs("Cannot get a menu's value!\n", stdout); return; } print_opt_value(opt); } void get_all_values(Entry *menu) { Entry *current; for (current = menu->children; current != NULL; current = current->next) { if (current->kind == KIND_OPTION) { print_opt_value(current); } } } void reset_value(char *command, Entry *menu) { Entry *opt = extract_num(&command, menu); if (opt == NULL) { return; } if (opt->kind != KIND_OPTION) { fputs("Cannot reset a menu's value!\n", stdout); return; } opt->val = opt->default_val; } void no_input(Entry *head) { fputs("You don't seem to be running under Fiwix or Linux currently.\n", stdout); fputs("Likely, you are currently running under builder-hex0.\n", stdout); fputs("That's ok! We're going to make some assumptions; namely, that you do need\n", stdout); fputs("the kernel bootstrap, and that you'll get a chance to configure later.\n", stdout); write_cfg_values(head, "KERNEL_BOOTSTRAP=True\nBUILD_KERNELS=True\n", FALSE); } int main(int argc, char **argv, char **envp) { /* * Check we are being non-interactive and bootstrap.cfg exists in * which case we do not need to do anything. */ char *interactivity = getenv("CONFIGURATOR"); if (interactivity != NULL) { if (strcmp(interactivity, "False") == 0) { return 0; } } FILE *bootstrap_cfg = fopen("/steps/bootstrap.cfg", "r"); if (bootstrap_cfg != NULL) { char *line = calloc(MAX_STRING, sizeof(char)); while (fgets(line, MAX_STRING, bootstrap_cfg) != NULL) { if (strcmp(line, "CONFIGURATOR=False\n") == 0) { fclose(bootstrap_cfg); return 0; } free(line); line = calloc(MAX_STRING, sizeof(char)); } fclose(bootstrap_cfg); } if (argc != 2) { fputs("Usage: ", stdout); fputs(argv[0], stdout); fputs(" \n", stdout); exit(1); } Entry *head = read_config(argv[1]); char *extra = set_cfg_values(head, envp); /* * Check if we are NOT running under fiwix or linux. * If we are not, and need configuration to occur, then we presume that * we will not be able to get any input from the user. */ struct utsname *kernel = calloc(1, sizeof(struct utsname)); uname(kernel); if (kernel->sysname == NULL) { no_input(head); return 0; } else if (strcmp(kernel->sysname, "Linux") != 0 && strcmp(kernel->sysname, "Fiwix") != 0) { no_input(head); return 0; } fputs("Welcome to live-bootstrap!\n", stdout); fputs("We need to do some brief configuration before continuing.\n\n", stdout); how_to_use(); fputc('\n', stdout); Entry *menu = head; print_menu(menu, menu == head); char *command = calloc(MAX_STRING, sizeof(char)); fputs("\nCommand: ", stdout); fflush(stdout); command = fgets(command, MAX_STRING, stdin); while (command != NULL) { if (strcmp("h\n", command) == 0 || strcmp("help\n", command) == 0) { how_to_use(); } else if (strcmp("l\n", command) == 0 || strcmp("list\n", command) == 0) { print_menu(menu, menu == head); } else if (strncmp("o ", command, 2) == 0 || strncmp("open ", command, 5) == 0) { menu = submenu(command, menu, head); print_menu(menu, menu == head); } else if (strncmp("? ", command, 2) == 0 || strncmp("describe ", command, 9) == 0) { print_description(command, menu); } else if (strcmp("g all\n", command) == 0 || strcmp("get all\n", command) == 0) { get_all_values(menu); } else if (strncmp("g ", command, 2) == 0 || strncmp("get ", command, 4) == 0) { get_opt_value(command, menu); } else if (strncmp("s ", command, 2) == 0 || strncmp("set ", command, 4) == 0) { set_opt_value(command, menu); } else if (strncmp("r ", command, 2) == 0 || strncmp("reset ", command, 6) == 0) { reset_value(command, menu); } else if (strcmp("e\n", command) == 0 || strcmp("exit\n", command) == 0) { if (!any_unset(head)) { break; } } else { fputs("Unknown command ", stdout); fputs(command, stdout); } fputs("\nCommand: ", stdout); fflush(stdout); /* * M2-Planet's fgets does not properly terminate the buffer if there is * already data in it */ free(command); command = calloc(MAX_STRING, sizeof(char)); command = fgets(command, MAX_STRING, stdin); } if (any_unset(head)) { fputs( "Uh oh! You have left me in a tough position - you can't input further because you\n" "closed the input stream. But the inputs you gave me are not valid!\n" "I'm going to re-exec myself and hope you are able to start again from scratch.\n", stderr ); execve(argv[0], argv, envp); return 0; } write_cfg_values(head, extra, TRUE); fputs("\nThank you! We will now continue with the bootstrap.\n", stdout); return 0; }