rockbox/apps/plugins/multiboot_select.c

347 lines
9.1 KiB
C
Raw Normal View History

/***************************************************************************
* __________ __ ___.
* 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;
}