522 lines
15 KiB
C++
522 lines
15 KiB
C++
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <csignal>
|
|
#include <csetjmp>
|
|
#include <functional>
|
|
#include <filesystem>
|
|
#include <getopt.h>
|
|
namespace fs = std::filesystem;
|
|
|
|
#include <readline/readline.h>
|
|
#include <readline/history.h>
|
|
#include <fmt/core.h>
|
|
#include <fmt/color.h>
|
|
|
|
#include "session.h"
|
|
#include "shell.h"
|
|
#include "theme.h"
|
|
#include "parser.h"
|
|
#include "legacy.h"
|
|
#include "errors.h"
|
|
#include "commands.h"
|
|
#include <fxos/util/log.h>
|
|
#include <fxos/semantics.h>
|
|
|
|
static std::map<std::string, ShellCommand *> commands;
|
|
|
|
ShellCommand::ShellCommand(std::string _name, ShellFunction _function,
|
|
ShellCompleter _completer, std::string _shortd, std::string _longd):
|
|
function {_function},
|
|
completer {_completer}, short_description {_shortd}
|
|
{
|
|
if(_longd.size()) {
|
|
/* Left trim */
|
|
_longd.erase(0, _longd.find_first_not_of("\n"));
|
|
/* Right trim */
|
|
int r = _longd.size();
|
|
while(r > 0 && _longd[--r] == '\n') {}
|
|
_longd.erase(r + 1, _longd.size());
|
|
}
|
|
|
|
this->long_description = _longd;
|
|
commands[_name] = this;
|
|
}
|
|
|
|
static Session global_session;
|
|
|
|
//---
|
|
// Autocompletion
|
|
//---
|
|
|
|
std::vector<std::string> complete_command(char const *text)
|
|
{
|
|
std::vector<std::string> options;
|
|
for(auto const &it: commands) {
|
|
if(!strncmp(it.first.c_str(), text, strlen(text)))
|
|
options.push_back(it.first);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
std::vector<std::string> complete_vspace(char const *text, Session &session)
|
|
{
|
|
std::vector<std::string> options;
|
|
for(auto const &it: session.spaces) {
|
|
if(!strncmp(it.first.c_str(), text, strlen(text)))
|
|
options.push_back(it.first);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
std::vector<std::string> complete_region(char const *text)
|
|
{
|
|
std::vector<std::string> options;
|
|
for(auto const &it: MemoryRegion::all()) {
|
|
if(!strncmp(it->name.c_str(), text, strlen(text)))
|
|
options.push_back(it->name);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
std::vector<std::string> complete_symbol(char const *text, VirtualSpace *space)
|
|
{
|
|
std::vector<std::string> options;
|
|
for(auto const &it: space->symbols.symbols) {
|
|
if(!strncmp(it.name.c_str(), text, strlen(text)))
|
|
options.push_back(it.name);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
Parser::CompletionRequest parse_until_autocomplete(
|
|
Session &session, char const *line_buffer, int point)
|
|
{
|
|
/* Parse partial input and try to get a category of suggestions */
|
|
std::string input(line_buffer, point);
|
|
lex_repl(input);
|
|
Parser p(true);
|
|
p.start();
|
|
|
|
/* Read commands until the parser runs out of input and throws a
|
|
CompletionRequest */
|
|
try {
|
|
do {
|
|
p.skip_separators();
|
|
std::string cmd = p.symbol("command");
|
|
if(commands.count(cmd) && commands[cmd]->completer)
|
|
commands[cmd]->completer(session, p);
|
|
}
|
|
while(!lex_idle());
|
|
}
|
|
catch(Parser::CompletionRequest r) {
|
|
return r;
|
|
}
|
|
catch(SyntaxError &e) {
|
|
/* Ignore syntax errors, just don't autocomplete */
|
|
}
|
|
|
|
return Parser::CompletionRequest("", "");
|
|
}
|
|
|
|
char *autocomplete(char const *text, int state)
|
|
{
|
|
static std::vector<std::string> options;
|
|
static size_t i = 0;
|
|
|
|
if(state == 0) {
|
|
Parser::CompletionRequest const &r = parse_until_autocomplete(
|
|
global_session, rl_line_buffer, rl_point);
|
|
if(r.category == "command")
|
|
options = complete_command(text);
|
|
// TODO: Replace vspace_name with binary_name in autocomplete
|
|
// TODO: Add legacy_vspace_name in autocomplete
|
|
else if(r.category == "vspace_name")
|
|
options = complete_vspace(text, global_session);
|
|
else if(r.category == "memory_region")
|
|
options = complete_region(text);
|
|
else if(r.category == "symbol" && r.space != nullptr)
|
|
options = complete_symbol(text, r.space);
|
|
else if(r.category == "_error") {
|
|
options.clear();
|
|
options.push_back("(ERROR) " + r.value);
|
|
options.push_back(".");
|
|
}
|
|
else
|
|
options.clear();
|
|
i = 0;
|
|
}
|
|
|
|
return (i < options.size()) ? strdup(options[i++].c_str()) : NULL;
|
|
}
|
|
|
|
//---
|
|
// Shell routine
|
|
//---
|
|
|
|
static bool read_interactive(Session &s, std::string &cmdline)
|
|
{
|
|
std::string prompt;
|
|
|
|
if(s.hasProject()) {
|
|
Project &p = s.project();
|
|
prompt += p.name() + (p.isDirty() ? "*" : "");
|
|
}
|
|
|
|
std::string binary_name = s.currentBinaryName();
|
|
if(binary_name == "")
|
|
binary_name = "(none)";
|
|
prompt += fmt::format("|{}> ", binary_name);
|
|
|
|
/* We need to insert RL_PROMPT_{START,END}_IGNORE into the color
|
|
formatting, so we trick a little bit by using a space */
|
|
std::string color = fmt::format(theme(9), " ");
|
|
int space_pos = color.find(' ');
|
|
std::string SC = color.substr(0, space_pos);
|
|
std::string EC = color.substr(space_pos + 1);
|
|
|
|
std::string SI(1, RL_PROMPT_START_IGNORE);
|
|
std::string EI(1, RL_PROMPT_END_IGNORE);
|
|
|
|
prompt = SI + SC + EI + prompt + SI + EC + EI;
|
|
|
|
/* Get a command to execute */
|
|
char *cmd_ptr = readline(prompt.c_str());
|
|
if(!cmd_ptr)
|
|
return true;
|
|
|
|
cmdline = std::string(cmd_ptr);
|
|
if(strlen(cmd_ptr) > 0)
|
|
add_history(cmd_ptr);
|
|
free(cmd_ptr);
|
|
return false;
|
|
}
|
|
|
|
char const *help_message =
|
|
R"(Reverse-engineering tool for SuperH programs.
|
|
|
|
usage: fxos [<FOLDER>|<FILE>|-p <NAME>] [OPTIONS]...
|
|
|
|
With <FOLDER>, open a project.
|
|
With <FILE>, create a new project and load a binary file.
|
|
With -p <NAME>, open a recent project by name.
|
|
With no argument, create a new empty project.
|
|
|
|
Options:
|
|
-l, --log=LEVEL Set log level (debug, warning, error; default warning)
|
|
-e, --execute=CMD Execute CMD and exit (can be specified multiple times)
|
|
)";
|
|
|
|
struct CLIOptions
|
|
{
|
|
int norc = 0;
|
|
std::string execute_arg = "";
|
|
char *extra_rom = NULL;
|
|
int log_level = FxOS::LOG_LEVEL_WRN;
|
|
|
|
std::string load = "";
|
|
bool load_is_by_name = false;
|
|
};
|
|
|
|
static struct CLIOptions parse_options(int argc, char **argv)
|
|
{
|
|
struct CLIOptions opts;
|
|
int show_help = 0;
|
|
|
|
// clang-format off
|
|
static struct option long_options[] = {
|
|
{ "help", no_argument, &show_help, 1 },
|
|
{ "execute", required_argument, NULL, 'e' },
|
|
{ "norc", no_argument, &opts.norc, 1 },
|
|
{ "log", required_argument, NULL, 'l' },
|
|
{ 0, 0, 0, 0 },
|
|
};
|
|
// clang-format on
|
|
|
|
while(1) {
|
|
/* Get the next command-line option */
|
|
int opt = getopt_long(argc, argv, "he:l:p:", long_options, NULL);
|
|
|
|
if(opt == -1)
|
|
break;
|
|
else if(opt == 'e') {
|
|
opts.execute_arg += ";";
|
|
opts.execute_arg += optarg;
|
|
}
|
|
else if(opt == 'l') {
|
|
if(!strcmp(optarg, "debug"))
|
|
opts.log_level = FxOS::LOG_LEVEL_LOG;
|
|
else if(!strcmp(optarg, "warning"))
|
|
opts.log_level = FxOS::LOG_LEVEL_LOG;
|
|
else if(!strcmp(optarg, "error"))
|
|
opts.log_level = FxOS::LOG_LEVEL_ERR;
|
|
else {
|
|
FxOS_log(ERR, "invalid log level '%s'", optarg);
|
|
printf("Try %s --help for more information.\n", argv[0]);
|
|
exit(1);
|
|
}
|
|
}
|
|
else if(opt == 'p') {
|
|
opts.load = optarg;
|
|
opts.load_is_by_name = true;
|
|
}
|
|
else if(opt != 0) {
|
|
printf("Try %s --help for more information.\n", argv[0]);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/* Print help message if --help is used at least once */
|
|
if(show_help) {
|
|
printf("%s", help_message);
|
|
exit(0);
|
|
}
|
|
|
|
/* Handle positional arguments */
|
|
if(optind < argc) {
|
|
if((argc - optind) > 1) {
|
|
printf("%s only supports 1 positional argument.\n", argv[0]);
|
|
printf("Try %s --help for more information.\n", argv[0]);
|
|
exit(1);
|
|
}
|
|
if(opts.load != "") {
|
|
FxOS_log(ERR, "multiple files/projects specified");
|
|
printf("Try %s --help for more information.\n", argv[0]);
|
|
exit(1);
|
|
}
|
|
opts.load = argv[optind++];
|
|
}
|
|
|
|
return opts;
|
|
}
|
|
|
|
static void load_initial_project(
|
|
Session &session, std::string input, bool isProjectName)
|
|
{
|
|
/* No load requested; let main() create a fresh project. */
|
|
if(input == "")
|
|
return;
|
|
|
|
/* Load recent project by name */
|
|
if(isProjectName) {
|
|
std::string path = session.recentProjects().pathFromName(input);
|
|
if(path == "")
|
|
FxOS_log(ERR, "No recent project named “%s”", input.c_str());
|
|
else
|
|
session.loadProject(path);
|
|
return;
|
|
}
|
|
|
|
/* Determine type of named input */
|
|
std::error_code rc;
|
|
fs::file_status st = fs::status(input, rc);
|
|
if(rc) {
|
|
FxOS_log(
|
|
ERR, "Cannot stat '%s': %s\n", input.c_str(), rc.message().c_str());
|
|
return;
|
|
}
|
|
if(st.type() == fs::file_type::not_found) {
|
|
FxOS_log(ERR, "'%s' does not exist\n", input.c_str());
|
|
return;
|
|
}
|
|
|
|
/* Load project by folder */
|
|
if(st.type() == fs::file_type::directory) {
|
|
session.loadProject(input);
|
|
return;
|
|
}
|
|
|
|
/* Create empty project for a given file */
|
|
if(st.type() == fs::file_type::regular) {
|
|
fmt::print("TODO: Create empty project with file in it\n");
|
|
return;
|
|
}
|
|
|
|
FxOS_log(ERR, "'%s' is not a regular file or folder", input.c_str());
|
|
}
|
|
|
|
static void global_help(void)
|
|
{
|
|
for(auto const &it: commands) {
|
|
fmt::print(" {:5s} {}\n", it.first, it.second->short_description);
|
|
}
|
|
}
|
|
|
|
static void command_help(std::string const &cmd)
|
|
{
|
|
fmt::print("{}: {}\n\n{}\n", cmd, commands[cmd]->short_description,
|
|
commands[cmd]->long_description);
|
|
}
|
|
|
|
static void debug_dump_bson(std::string const &path)
|
|
{
|
|
BSON b = BSON::loadDocumentFromFile(path, true, true);
|
|
b.dump(stdout);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
/* Parse command-line options first */
|
|
struct CLIOptions opts = parse_options(argc, argv);
|
|
FxOS::log_setminlevel(opts.log_level);
|
|
|
|
Session &s = global_session;
|
|
s.loadConfig(std::string(std::getenv("HOME")) + "/.config/fxos/fxos.bin");
|
|
s.recentProjects().refresh();
|
|
|
|
theme_builtin("tomorrow-night");
|
|
|
|
rl_completion_entry_function = autocomplete;
|
|
rl_basic_word_break_characters
|
|
= " \t\n\"\\'`@$><=;|&{(" /* Readline's default */
|
|
"+-*%)"; /* Word breaks with special characters in fxos */
|
|
|
|
/* Load path into the session */
|
|
char const *fxos_path_env = std::getenv("FXOS_PATH");
|
|
if(fxos_path_env) {
|
|
std::string fxos_path = fxos_path_env;
|
|
|
|
size_t offset = 0, end;
|
|
while(true) {
|
|
offset = fxos_path.find_first_not_of(":", offset);
|
|
if(offset >= fxos_path.size())
|
|
break;
|
|
|
|
end = fxos_path.find_first_of(":", offset);
|
|
s.path.push_back(fxos_path.substr(offset, end - offset));
|
|
offset = end;
|
|
}
|
|
}
|
|
else {
|
|
FxOS_log(WRN, "no FXOS_PATH in environment, using working directory");
|
|
s.path.push_back(fs::current_path());
|
|
}
|
|
|
|
/* Clear readline input when receiving SIGINT */
|
|
static sigjmp_buf sigint_buf;
|
|
std::signal(SIGINT, [](int) {
|
|
rl_free_line_state();
|
|
rl_cleanup_after_signal();
|
|
rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0;
|
|
printf("\n");
|
|
siglongjmp(sigint_buf, 1);
|
|
});
|
|
rl_set_signals();
|
|
|
|
/* Load command history */
|
|
char histfile[128] = "";
|
|
if(std::getenv("HOME"))
|
|
snprintf(histfile, 128, "%s/.fxos_history", std::getenv("HOME"));
|
|
if(histfile[0])
|
|
read_history(histfile);
|
|
|
|
/* Load a project as specified by command-line arguments */
|
|
load_initial_project(s, opts.load, opts.load_is_by_name);
|
|
/* If none was given or it failed, load the most recent project */
|
|
if(!s.hasProject()) {
|
|
fs::path const &p = s.recentProjects().mostRecentPath();
|
|
if(p != "")
|
|
s.loadProject(p);
|
|
}
|
|
/* If that failed too, create a blank project */
|
|
if(!s.hasProject())
|
|
s.switchToNewProject();
|
|
|
|
/* Load fxosrc files from all library folders if wanted */
|
|
if(!opts.norc) {
|
|
std::vector<std::string> fxosrc_files;
|
|
for(auto const &path: s.path) {
|
|
if(fs::exists(path / "fxosrc"))
|
|
fxosrc_files.push_back(path / "fxosrc");
|
|
}
|
|
if(fxosrc_files.size() > 0) {
|
|
_dot(s, fxosrc_files, true);
|
|
FxOS_log(WRN, "Legacy fxosrc files found; use pm to migrate.");
|
|
}
|
|
}
|
|
|
|
/* Whether we have reached an idle state after parsing fxosrc files */
|
|
bool has_idled = false;
|
|
/* Command line input by user */
|
|
std::string cmdline;
|
|
|
|
while(true) {
|
|
/* Handle initial fxosrc files and the CLI --execute option */
|
|
if(lex_idle()) {
|
|
if(opts.execute_arg != "") {
|
|
if(has_idled)
|
|
exit(0);
|
|
else
|
|
lex_repl(opts.execute_arg);
|
|
}
|
|
has_idled = true;
|
|
}
|
|
/* Or read from the command line */
|
|
if(lex_idle()) {
|
|
while(sigsetjmp(sigint_buf, 1) != 0) {}
|
|
cmdline = "";
|
|
if(read_interactive(s, cmdline) && s.confirmProjectUnload())
|
|
break;
|
|
lex_repl(cmdline);
|
|
has_idled = true;
|
|
}
|
|
|
|
Parser parser(false);
|
|
|
|
try {
|
|
/* Read the next command from the lexer */
|
|
parser.start();
|
|
parser.skip_separators();
|
|
|
|
/* Read the command name */
|
|
if(parser.lookahead().type == T::END)
|
|
continue;
|
|
if(parser.lookahead().type == '?') {
|
|
global_help();
|
|
continue;
|
|
}
|
|
std::string cmd = parser.symbol("command");
|
|
|
|
if(cmd == "q" && parser.lookahead().type != '?'
|
|
&& s.confirmProjectUnload())
|
|
break;
|
|
else if(cmd == "_p") {
|
|
parser.dump_command();
|
|
}
|
|
else if(cmd == "_b") {
|
|
std::string path = parser.str();
|
|
parser.end();
|
|
debug_dump_bson(path);
|
|
}
|
|
else if(!has_idled && legacy_command(s, cmd, parser)) {
|
|
continue;
|
|
}
|
|
else if(commands.count(cmd)) {
|
|
if(parser.lookahead().type == '?')
|
|
command_help(cmd);
|
|
else if(commands[cmd]->function)
|
|
commands[cmd]->function(s, parser);
|
|
}
|
|
else {
|
|
FxOS_log(ERR, "unknown command '%s'", cmd);
|
|
}
|
|
}
|
|
catch(std::exception &e) {
|
|
FxOS_log(ERR, "%s", e.what());
|
|
}
|
|
|
|
/* Exhaust command input (if not all used by the command) */
|
|
parser.exhaust_until_separator();
|
|
}
|
|
|
|
/* Save command history */
|
|
if(histfile[0])
|
|
write_history(histfile);
|
|
|
|
s.saveConfig();
|
|
return 0;
|
|
}
|
|
|
|
/* Register a no-op quit command for auto-completion */
|
|
static ShellCommand _q_cmd("q", NULL, NULL, "Quit", "Quits fxos.");
|