summaryrefslogtreecommitdiffstats
path: root/apps/plugins/multiboot_select.c
diff options
context:
space:
mode:
Diffstat (limited to 'apps/plugins/multiboot_select.c')
-rw-r--r--apps/plugins/multiboot_select.c346
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;
+}