diff options
Diffstat (limited to 'apps/plugins/multiboot_select.c')
-rw-r--r-- | apps/plugins/multiboot_select.c | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/apps/plugins/multiboot_select.c b/apps/plugins/multiboot_select.c new file mode 100644 index 0000000000..63babbb2c1 --- /dev/null +++ b/apps/plugins/multiboot_select.c @@ -0,0 +1,346 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2022 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" + +/* should be more than enough */ +#define MAX_ROOTS 128 + +static enum plugin_status plugin_status = PLUGIN_OK; +static char tmpbuf[MAX_PATH+1]; +static char tmpbuf2[MAX_PATH+1]; +static char cur_root[MAX_PATH]; +static char roots[MAX_ROOTS][MAX_PATH]; +static int nroots; + +/* Read a redirect file and return the path */ +static char* read_redirect_file(const char* filename) +{ + int fd = rb->open(filename, O_RDONLY); + if(fd < 0) + return NULL; + + ssize_t ret = rb->read(fd, tmpbuf, sizeof(tmpbuf)); + rb->close(fd); + if(ret < 0 || (size_t)ret >= sizeof(tmpbuf)) + return NULL; + + /* relative paths are ignored */ + if(tmpbuf[0] != '/') + ret = 0; + + /* remove trailing control chars (newlines etc.) */ + for(ssize_t i = ret - 1; i >= 0 && tmpbuf[i] < 0x20; --i) + tmpbuf[i] = '\0'; + + return tmpbuf; +} + +/* Search for a redirect file, like get_redirect_dir() */ +static const char* read_redirect(void) +{ + for(int vol = NUM_VOLUMES-1; vol >= MULTIBOOT_MIN_VOLUME; --vol) { + rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR); + const char* path = read_redirect_file(tmpbuf); + if(path && path[0] == '/') { + /* prepend the volume because that's what we expect */ + rb->snprintf(tmpbuf2, sizeof(tmpbuf2), "/<%d>%s", vol, path); + return tmpbuf2; + } + } + + tmpbuf[0] = '\0'; + return tmpbuf; +} + +/* Remove all redirect files except for the one on volume 'exclude_vol' */ +static int clear_redirect(int exclude_vol) +{ + int ret = 0; + for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) { + if(vol == exclude_vol) + continue; + + rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR); + if(rb->file_exists(tmpbuf) && rb->remove(tmpbuf) < 0) + ret = -1; + } + + return ret; +} + +/* Save a path to the redirect file */ +static int write_redirect(const char* abspath) +{ + /* get the volume (required) */ + const char* path = abspath; + int vol = rb->path_strip_volume(abspath, &path, false); + if(path == abspath) + return -1; + + /* remove all other redirect files */ + if(clear_redirect(vol)) + return -1; + + /* open the redirect file on the same volume */ + rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR); + int fd = rb->open(tmpbuf, O_WRONLY|O_CREAT|O_TRUNC, 0644); + if(fd < 0) + return fd; + + size_t pathlen = rb->strlen(path); + ssize_t ret = rb->write(fd, path, pathlen); + if(ret < 0 || (size_t)ret != pathlen) { + rb->close(fd); + return -1; + } + + ret = rb->write(fd, "\n", 1); + rb->close(fd); + if(ret != 1) + return -1; + + return 0; +} + +/* Check if the firmware file is valid + * TODO: this should at least check model number or something */ +static bool check_firmware(const char* path) +{ + return rb->file_exists(path); +} + +static int root_compare(const void* a, const void* b) +{ + const char* as = a; + const char* bs = b; + return rb->strcmp(as, bs); +} + +/* Scan the filesystem for possible redirect targets. To prevent this from + * taking too long we only check the directories in the root of each volume + * look check for a rockbox firmware binary underneath /dir/.rockbox. If it + * exists then we report /<vol#>/dir as a root. */ +static int find_roots(void) +{ + const char* bootdir = *BOOTDIR == '/' ? &BOOTDIR[1] : BOOTDIR; + nroots = 0; + + for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) { + rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>", vol); + + /* try to open the volume root; ignore failures since they'll + * occur if the volume is unmounted */ + DIR* dir = rb->opendir(tmpbuf); + if(!dir) + continue; + + struct dirent* ent; + while((ent = rb->readdir(dir))) { + int r = rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s/%s/%s", + vol, ent->d_name, bootdir, BOOTFILE); + if(r < 0 || (size_t)r >= sizeof(tmpbuf)) + continue; + + if(check_firmware(tmpbuf)) { + rb->snprintf(roots[nroots], MAX_PATH, "/<%d>/%s", + vol, ent->d_name); + nroots += 1; + + /* quit if we hit the maximum */ + if(nroots == MAX_ROOTS) { + vol = NUM_VOLUMES; + break; + } + } + } + + rb->closedir(dir); + } + + rb->qsort(roots, nroots, MAX_PATH, root_compare); + return nroots; +} + +static const char* picker_get_name_cb(int selected, void* data, + char* buffer, size_t buffer_len) +{ + (void)data; + (void)buffer; + (void)buffer_len; + return roots[selected]; +} + +static int picker_action_cb(int action, struct gui_synclist* lists) +{ + (void)lists; + if(action == ACTION_STD_OK) + action = ACTION_STD_CANCEL; + return action; +} + +static bool show_picker_menu(int* selection) +{ + struct simplelist_info info; + rb->simplelist_info_init(&info, "Select new root", nroots, NULL); + info.selection = *selection; + info.get_name = picker_get_name_cb; + info.action_callback = picker_action_cb; + if(rb->simplelist_show_list(&info)) + return true; + + if(0 <= info.selection && info.selection < nroots) + *selection = info.selection; + + return false; +} + +enum { + MB_SELECT_ROOT, + MB_CLEAR_REDIRECT, + MB_CURRENT_ROOT, + MB_SAVE_AND_EXIT, + MB_SAVE_AND_REBOOT, + MB_EXIT, + MB_NUM_ITEMS, +}; + +static const char* menu_get_name_cb(int selected, void* data, + char* buffer, size_t buffer_len) +{ + (void)data; + + switch(selected) { + case MB_SELECT_ROOT: + return "Select root"; + + case MB_CLEAR_REDIRECT: + return "Clear redirect"; + + case MB_CURRENT_ROOT: + if(cur_root[0]) { + rb->snprintf(buffer, buffer_len, "Using root: %s", cur_root); + return buffer; + } + + return "Using default root"; + + case MB_SAVE_AND_EXIT: + return "Save and Exit"; + + case MB_SAVE_AND_REBOOT: + return "Save and Reboot"; + + case MB_EXIT: + default: + return "Exit"; + } +} + +static int menu_action_cb(int action, struct gui_synclist* lists) +{ + int selected = rb->gui_synclist_get_sel_pos(lists); + + if(action != ACTION_STD_OK) + return action; + + switch(selected) { + case MB_SELECT_ROOT: { + if(find_roots() <= 0) { + rb->splashf(3*HZ, "No roots found"); + break; + } + + int root_sel = nroots-1; + for(; root_sel > 0; --root_sel) + if(!rb->strcmp(roots[root_sel], cur_root)) + break; + + if(show_picker_menu(&root_sel)) + return SYS_USB_CONNECTED; + + rb->strcpy(cur_root, roots[root_sel]); + action = ACTION_REDRAW; + } break; + + case MB_CLEAR_REDIRECT: + if(cur_root[0]) { + memset(cur_root, 0, sizeof(cur_root)); + rb->splashf(HZ, "Cleared redirect"); + } + + action = ACTION_REDRAW; + break; + + case MB_SAVE_AND_REBOOT: + case MB_SAVE_AND_EXIT: { + int ret; + if(cur_root[0]) + ret = write_redirect(cur_root); + else + ret = clear_redirect(-1); + + if(ret < 0) + rb->splashf(3*HZ, "Couldn't save settings"); + else + rb->splashf(HZ, "Settings saved"); + + action = ACTION_STD_CANCEL; + plugin_status = (ret < 0) ? PLUGIN_ERROR : PLUGIN_OK; + + if(ret >= 0 && selected == MB_SAVE_AND_REBOOT) + rb->sys_reboot(); + } break; + + case MB_EXIT: + return ACTION_STD_CANCEL; + + default: + action = ACTION_REDRAW; + break; + } + + return action; +} + +static bool show_menu(void) +{ + struct simplelist_info info; + rb->simplelist_info_init(&info, "Multiboot Settings", MB_NUM_ITEMS, NULL); + info.get_name = menu_get_name_cb; + info.action_callback = menu_action_cb; + return rb->simplelist_show_list(&info); +} + +enum plugin_status plugin_start(const void* param) +{ + (void)param; + + /* load the current root */ + const char* myroot = read_redirect(); + rb->strcpy(cur_root, myroot); + + /* display the menu */ + if(show_menu()) + return PLUGIN_USB_CONNECTED; + else + return plugin_status; +} |