fxos: add new project logic (without any data in them)

Projects are now created, saved, reloaded. Next step is to provide the
pm command to migrate old vspaces to them, and then start getting rid of
the old abstractions.
This commit is contained in:
Lephenixnoir 2023-09-23 19:11:52 +02:00
parent 6edbd1dba1
commit 356d09e52d
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
11 changed files with 966 additions and 83 deletions

View File

@ -43,6 +43,7 @@ set(fxos_core_SOURCES
lib/passes/pcrel.cpp
lib/passes/print.cpp
lib/passes/syscall.cpp
lib/project.cpp
lib/semantics.cpp
lib/symbols.cpp
lib/vspace.cpp
@ -76,6 +77,7 @@ set(fxos_shell_SOURCES
shell/h.cpp
shell/i.cpp
shell/m.cpp
shell/p.cpp
shell/s.cpp
shell/v.cpp
${FLEX_Lexer_OUTPUTS}

43
doc/program-structure.md Normal file
View File

@ -0,0 +1,43 @@
## Structure of programs in fxos
The loading and management of programs go through several layers that each have their own classes and concerns.
### Loading layer
```c++
#include <fxos/vspace.h>
#include <fxos/memory.h>
```
The loading layer is concerned with emulating the virtual address space where the program is living. The address space is handled by `VirtualSpace`, which contains one or more `Binding`s of files or buffers to virtual addresses. The main methods in `VirtualSpace` are the read and search methods from the `AbstractMemory` typeclass.
General information about the address space in SuperH MPUs is available through `<fxos/memory.h>`, which lists MMU areas and their permissions in `MemoryArea`, and standard memory regions/chips in `MemoryRegion`.
_(In previous versions of fxos, `VirtualSpace` was the entry point to find the disassembly, OS analysis, symbols, etc. This is now handled by `Binary`.)_
While not explicitly supported yet, the loading layer could be used to load compiled add-ins or ELF files.
### Binary layer
```c++
#include <fxos/binary.h>
#include <fxos/function.h>
#include <fxos/os.h>
```
The binary layer is concerned with reconstructing the source program from its binary code. The `Binary` object references a `VirtualSpace` and collects variable definitions, function definition, and analysis results.
Every reconstructed object in the program inherits from `BinaryObject`; this includes `Variable`, `Function` and the lightweight `Mark`. Functions are described in much greater detail in _[Functions](functions.md)_.
Analysis results at the binary layer include:
- OS analysis (in the future, maybe add-in analysis)
- The function call graph (WIP)
- Variable/function references and cross-references (WIP)
### Project layer
```c++
#include <fxos/project.h>
```
The project layer collects multiple binaries in a same project (typically several versions of the same program) along with cross-binary analysis results.

87
doc/projects.md Normal file
View File

@ -0,0 +1,87 @@
## Projects
Projects are fxos' way of managing data long-term by saving it to disk and reloading it. Projects allow virtual space settings, binary objects, analysis results and user annotations to be saved and reused across multiple sessions.
When fxos is running it always has exactly one project open at all times. It is visible in the prompt, which consists of the project name and the current binary. For instance in
```
unnamed_project*|cg_3.60>
```
the project name in `unnamed_project` and the currently-focused binary is `cg_3.60`. A star next to the project name indicates that it has unsaved changes.
### Contents
A project consists of
- A set of binaries (with their analysis results, annotations, etc);
- Cross-binary analysis results.
Projects are saved to disk as _folders_ with names usually ending in `.fxos`. fxos expects to manage the entire folder, so extra files should not be created in there. Since the format is binary serialization for convenience, it's also unlikely to be convenient to edit without using fxos directly.
### Recent projects
fxos remembers recently-opened projects. By default (when started with no arguments), fxos loads the most recent project.
The list of paths and associated names can be printed with the Info Projects (`ip`) command. It is updated automatically at startup, pruning projects that have been moved or removed. This list can be used to refer to projects by name instead of specifying their full paths, such as in the CLI option `-p`.
### Commands for managing projects
**`pn`: Project New**
```
pn [<name> ["<path>"]]
(1) pn
(2) pn cg_system
(3) pn cg_system "~/cg_system.fxos"
```
`pn` will create a new project and switch to it (if the current project has been saved). With no path argument (1,2), it generates a temporary project that must be given a path in `ps` the first time it is saved. If no name argument is provided, a placeholder name like `"unnamed_project"` is generated. With a path argument (3), the project is saved immediately.
**`pr`: Project Rename**
```
pr <new_name>
```
`pr` renames the current project. This does not change its path; to change the path, use `ps` to save to a new path then remove the old copy.
**`ps`: Project Save**
```
ps ["<path>"]
```
`ps` saves the current project. If no argument is specified, this saves to project's current path. With an argument, saves to a new folder (Save As). This can be used to copy or move projects.
**`pm`: Project Migrate**
```
pm <vspace_name>
```
`pm` migrates a "virtual space" defined by an `fxosrc` file to the project format. Previous versions of fxos used a global library of virtual spaces, loaded through a startup script called `fxosrc`, as makeshift projects. This was replaced in fxos 1.x by actual projects to allow saving analysis results.
In version 1.x, fxos still loads `fxosrc` if it exists, and lists any legacy virtual spaces in `ip`. `pm` imports a legacy virtual space, designated by name, into the current project as a new binary.
### Opening projects from the command line and fxos prompt
**Opening on the command-line**
```
(1) % fxos <path>/
(2) % fxos <file>
(3) % fxos -p <name>
```
When specifying an existing folder as argument (1), fxos assumes the folder is a project folder and attempts to load it. When specifying an existing file (2), a new temporary project is created, with that file mapped in the virtual sapce. When specifying `-p`, fxos loads the most recent project with the provided name.
**`pl`: Project Load**
```
pl (<name> | "<path>")
```
`pl` loads an existing project. With a symbol argument, loads a recent project by name. With string that identifies a folder, loads a project from the filesystem.

116
include/fxos/project.h Normal file
View File

@ -0,0 +1,116 @@
//---------------------------------------------------------------------------//
// 1100101 |_ mov #0, r4 __ //
// 11 |_ <0xb380 %5c4> / _|_ _____ ___ //
// 0110 |_ 3.50 -> 3.60 | _\ \ / _ (_-< //
// |_ base# + offset |_| /_\_\___/__/ //
//---------------------------------------------------------------------------//
// fxos/project: Project data and management
//
// This header defines the projet structures, with its binaries and analysis
// results. It also provides access to recent projects.
//---
#ifndef FXOS_PROJECT_H
#define FXOS_PROJECT_H
#include <fxos/binary.h>
#include <fxos/util/bson.h>
#include <string>
#include <vector>
#include <ctime>
namespace FxOS {
struct Project
{
/* Create an empty project with a default name and no path. */
Project();
std::string const &name() const
{
return m_name;
}
std::string const &path() const
{
return m_path;
}
/* Set a new name. The caller should also update recent projects. */
void setName(std::string const &new_name);
/* Set a new path (usually by specifying one in `ps`). The caller should
also update recent projects. */
void setPath(std::string const &new_path);
/* Whether the project can be saved (ie. has a path). */
bool canSave() const;
/* Whether the project is dirty and should be saved before closing. */
bool isDirty() const;
/* Mark the project as dirty. This should be called by commands. */
void setDirty();
/* Save; prints on stderr and returns false in case of error. */
bool save();
/* Load from a folder. */
bool load(std::string const &path);
private:
/* Project name (no constraints but typically an identifier) */
std::string m_name;
/* Absolute project path in the filesystem */
std::string m_path;
/* Whether project needs a confirmation/save before closing */
bool m_dirty;
// TODO: List of binaries
BSON serializeMetadata() const;
};
struct RecentProjects
{
struct RecentProjectEntry
{
/* Last known project name */
std::string name;
/* Absolute path */
std::string path;
/* Last use time */
std::time_t utime;
RecentProjectEntry() = default;
RecentProjectEntry(BSON const &);
BSON serialize() const;
};
RecentProjects() = default;
BSON serialize() const;
void deserialize(BSON const &);
/* Get list of all entries, most recent first. */
std::vector<RecentProjectEntry> const &entries() const
{
return m_entries;
}
/* Get project path from name (empty string if none found) */
std::string pathFromName(std::string const &name) const;
/* Get path of most recent project */
std::string mostRecentPath() const;
/* Remove a project from the list, if present. */
void remove(std::string const &path);
/* Create or update a project in the list. */
void touch(Project const &p);
/* Refresh list by removing projects that no longer exist. */
void refresh();
private:
std::vector<RecentProjectEntry> m_entries;
};
/* Global recent project register */
RecentProjects &recentProjects();
} /* namespace FxOS */
#endif /* FXOS_PROJECT_H */

185
lib/project.cpp Normal file
View File

@ -0,0 +1,185 @@
#include <fxos/project.h>
#include <fxos/util/log.h>
#include <filesystem>
#include <algorithm>
#include <string>
using namespace std::literals;
namespace fs = std::filesystem;
namespace FxOS {
//=== Project ===//
Project::Project()
{
m_name = "unnamed_project";
}
void Project::setName(std::string const &new_name)
{
m_name = new_name;
}
void Project::setPath(std::string const &new_path)
{
m_path = new_path;
}
bool Project::canSave() const
{
return m_path != "";
}
bool Project::isDirty() const
{
return m_dirty;
}
void Project::setDirty()
{
m_dirty = true;
}
BSON Project::serializeMetadata() const
{
return BSON::mkDocument({
{"*", BSON::mkString("Project")}, {"name", BSON::mkString(m_name)},
// TODO: List of binaries by name
});
}
bool Project::save()
{
fs::path path(m_path);
if(path == "") {
FxOS_log(ERR, "Project “%s” has no path", m_name.c_str());
return false;
}
std::error_code ec;
fs::create_directories(path, ec);
if(ec) {
FxOS_log(ERR, "Could not create path to '%s': %s", path.c_str(),
ec.message());
return false;
}
fs::path metadata_path = path / "project";
FILE *metadata = fopen(metadata_path.c_str(), "w");
if(!metadata) {
FxOS_log(ERR, "cannot write `%s': %m", metadata_path.c_str());
return false;
}
serializeMetadata().serialize(metadata);
fclose(metadata);
m_dirty = false;
return true;
}
bool Project::load(std::string const &path0)
{
fs::path path = path0;
BSON metadata
= BSON::loadDocumentFromFile(path / "project", true, true, "Project");
if(metadata.isNull())
return false;
m_path = path;
m_name = metadata["name"].getString();
m_dirty = false;
return true;
}
//=== RecentProjects ===//
RecentProjects::RecentProjectEntry::RecentProjectEntry(BSON const &b)
{
assert(b["*"].getStringReadOnly() == "RecentProjectEntry"s);
name = b["name"].getStringReadOnly();
path = b["path"].getStringReadOnly();
utime = b["utime"].getI64();
}
BSON RecentProjects::RecentProjectEntry::serialize() const
{
return BSON::mkDocument({
{"*", BSON::mkString("RecentProjectEntry")},
{"name", BSON::mkString(name)},
{"path", BSON::mkString(path)},
{"utime", BSON::mkI64(utime)},
});
}
BSON RecentProjects::serialize() const
{
BSON b = BSON::mkArray(m_entries.size());
for(uint i = 0; i < m_entries.size(); i++)
b[i] = m_entries[i].serialize();
return b;
}
void RecentProjects::deserialize(BSON const &b)
{
assert(b.isArray());
for(uint i = 0; i < b.size(); i++)
m_entries.push_back(RecentProjectEntry(b[i]));
}
std::string RecentProjects::pathFromName(std::string const &name) const
{
for(auto &entry: m_entries) {
if(entry.name == name)
return entry.path;
}
return "";
}
std::string RecentProjects::mostRecentPath() const
{
if(!m_entries.size())
return "";
return std::max_element(m_entries.begin(), m_entries.end(),
[](RecentProjectEntry const &e1, RecentProjectEntry const &e2) {
return e1.utime < e2.utime;
})
->path;
}
void RecentProjects::remove(std::string const &path)
{
auto it = m_entries.begin();
while(it != m_entries.end()) {
if(it->path == path)
it = m_entries.erase(it);
else
++it;
}
}
void RecentProjects::touch(Project const &p)
{
remove(p.path());
struct RecentProjectEntry entry;
entry.name = p.name();
entry.path = p.path();
entry.utime = time(NULL);
m_entries.insert(m_entries.begin(), entry);
}
void RecentProjects::refresh()
{
auto it = std::remove_if(
m_entries.begin(), m_entries.end(), [](RecentProjectEntry const &e) {
std::error_code rc;
fs::file_status st = fs::status(e.path, rc);
return rc || st.type() != fs::file_type::directory;
});
m_entries.erase(it, m_entries.end());
}
} /* namespace FxOS */

View File

@ -6,6 +6,7 @@
#include <algorithm>
#include <fmt/core.h>
#include <fmt/chrono.h>
#include <fxos/util/log.h>
//---
@ -187,6 +188,33 @@ void _io(Session &session, std::string name)
printf(" (none)\n");
}
//---
// ip
//---
void _ip(Session &session)
{
Project *p = session.project();
if(!p)
fmt::print("No current project o(x_x)o\n");
else {
fmt::print(
"Current project: {}{}\n", p->name(), p->isDirty() ? "*" : "");
if(p->path() != "")
fmt::print("Path: {}\n", p->path());
else
fmt::print("No path yet (use ps to assign one)\n");
}
auto const &entries = session.recentProjects().entries();
fmt::print("\nRecent projects:\n");
if(!entries.size())
fmt::print(" (none)\n");
for(auto &e: entries)
fmt::print(" {} ({}, last used {:%c})\n", e.name, e.path,
fmt::localtime(e.utime));
}
//---
// isc
//---
@ -464,6 +492,15 @@ Prints information about the OS mapped in the named virtual space (defaults to
the current one). This usually requires an OS binary to be mapped to ROM.
)");
static ShellCommand _ip_cmd(
"ip", [](Session &s, Parser &) { _ip(s); },
[](Session &, Parser &p) { p.end(); }, "Info Projects", R"(
ip
Projects information about the currently-loaded project, and paths and names of
recent projects;
)");
static ShellCommand _isc_cmd(
"isc",
[](Session &s, Parser &p) {

View File

@ -4,7 +4,9 @@
#include <csignal>
#include <csetjmp>
#include <functional>
#include <filesystem>
#include <getopt.h>
namespace fs = std::filesystem;
#include <readline/readline.h>
#include <readline/history.h>
@ -149,7 +151,7 @@ char *autocomplete(char const *text, int state)
// Shell routine
//---
static std::string read_interactive(Session const &s, bool &leave)
static std::string read_interactive(Session &s, bool &leave)
{
std::string prompt = "no_vspace> ";
if(s.current_space) {
@ -160,7 +162,13 @@ static std::string read_interactive(Session const &s, bool &leave)
name = it.first;
}
prompt = fmt::format("{} @ 0x{:08x}> ", name, s.current_space->cursor);
Project *p = s.project();
if(!p)
prompt
= fmt::format("{} @ 0x{:08x}> ", name, s.current_space->cursor);
else
prompt = fmt::format(
"{}{}|{}> ", p->name(), p->isDirty() ? "*" : "", name);
}
/* We need to insert RL_PROMPT_{START,END}_IGNORE into the color
@ -189,72 +197,75 @@ static std::string read_interactive(Session const &s, bool &leave)
return cmd;
}
int norc = 0;
std::string execute_arg = "";
char *extra_rom = NULL;
int log_level = FxOS::LOG_LEVEL_WRN;
char const *help_message =
R"(Usage: fxos [OPTION]... [FILE]
R"(Reverse-engineering tool for SuperH programs.
Open the fxos disassembler, optionally with [FILE] mapped into a new empty
space at ROM and ROM_P2.
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:
-e, --execute=CMD execute CMD and exit, can be used multiple times
--norc do not execute any fxosrc files
-l, --log=LEVEL set log level (debug, warning, error; default warning)
-h, --help display this help and exit
-l, --log=LEVEL Set log level (debug, warning, error; default warning)
-e, --execute=CMD Execute CMD and exit (can be specified multiple times)
)";
static void parse_options(int argc, char **argv)
struct CLIOptions
{
/* List of options */
static struct option long_options[] = {{"help", no_argument, 0, 'h'},
{"execute", required_argument, 0, 'e'}, {"norc", no_argument, &norc, 1},
{"log", required_argument, 0, 'l'},
/* This is here as a fallback if nothing is passed */
{0, 0, 0, 0}};
int norc = 0;
std::string execute_arg = "";
char *extra_rom = NULL;
int log_level = FxOS::LOG_LEVEL_WRN;
bool show_help = false;
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
int opt, option_index;
while(1) {
/* Get the next command-line option */
opt = getopt_long(argc, argv, "he:l:", long_options, &option_index);
int opt = getopt_long(argc, argv, "he:l:p:", long_options, NULL);
if(opt == -1)
break;
/* Handle options */
switch(opt) {
case 0:
/* This happens if we store the argument to a variable (e.g. --norc) */
break;
case 'h':
show_help = true;
break;
case 'e':
execute_arg += ";";
execute_arg += optarg;
break;
case 'l':
else if(opt == 'e') {
opts.execute_arg += ";";
opts.execute_arg += optarg;
}
else if(opt == 'l') {
if(!strcmp(optarg, "debug"))
log_level = FxOS::LOG_LEVEL_LOG;
opts.log_level = FxOS::LOG_LEVEL_LOG;
else if(!strcmp(optarg, "warning"))
log_level = FxOS::LOG_LEVEL_LOG;
opts.log_level = FxOS::LOG_LEVEL_LOG;
else if(!strcmp(optarg, "error"))
log_level = FxOS::LOG_LEVEL_ERR;
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);
}
break;
case '?':
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
default:
}
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);
}
@ -269,22 +280,75 @@ static void parse_options(int argc, char **argv)
/* Handle positional arguments */
if(optind < argc) {
if((argc - optind) > 1) {
printf("%s only supports 1 positional argument, FILE.\n", argv[0]);
printf("%s only supports 1 positional argument.\n", argv[0]);
printf("Try %s --help for more information.\n", argv[0]);
exit(1);
}
extra_rom = argv[optind++];
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());
}
int main(int argc, char **argv)
{
/* Parse command-line options first */
parse_options(argc, argv);
FxOS::log_setminlevel(log_level);
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");
@ -310,7 +374,7 @@ int main(int argc, char **argv)
}
}
else {
fmt::print("warning: no FXOS_PATH in environment, using WD\n");
FxOS_log(WRN, "no FXOS_PATH in environment, using working directory");
s.path.push_back(fs::current_path());
}
@ -332,8 +396,20 @@ int main(int argc, char **argv)
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.project()) {
fs::path const &p = s.recentProjects().mostRecentPath();
if(p != "")
s.loadProject(p);
}
/* If that failed too, create a blank project */
if(!s.project())
s.switchToNewProject();
/* Load fxosrc files from all library folders if wanted */
if(!norc) {
if(!opts.norc) {
std::vector<std::string> fxosrc_files;
for(auto const &path: s.path) {
if(fs::exists(path / "fxosrc"))
@ -343,39 +419,27 @@ int main(int argc, char **argv)
_dot(s, fxosrc_files, true);
}
/* Stores whether we have already idled once, handling extra rom
* and execute options */
and execute options */
bool has_idled = false;
/* Shell main loop */
while(true) {
/* Get a command if there isn't a file being executed */
if(lex_idle()) {
/* If we passed in FILE and we haven't handled it yet */
if(extra_rom && !has_idled) {
/* Create new 'file' virtual space and switch to it */
_vc(s, "file");
_vs(s, "file");
/* Add FILE as a ROM and ROM_P2 */
_vm(s, extra_rom, {MemoryRegion::ROM, MemoryRegion::ROM_P2});
}
/* If we need to execute a command */
if(!execute_arg.empty()) {
if(!opts.execute_arg.empty()) {
if(has_idled)
exit(0);
else
lex_repl(execute_arg);
lex_repl(opts.execute_arg);
}
else {
while(sigsetjmp(sigint_buf, 1) != 0)
;
while(sigsetjmp(sigint_buf, 1) != 0) {}
bool leave = false;
std::string cmdline = read_interactive(s, leave);
if(leave)
if(leave && s.confirmProjectUnload())
break;
lex_repl(cmdline);
}
@ -403,7 +467,8 @@ int main(int argc, char **argv)
}
std::string cmd = parser.symbol("command");
if(cmd == "q" && parser.lookahead().type != '?')
if(cmd == "q" && parser.lookahead().type != '?'
&& s.confirmProjectUnload())
break;
if(cmd == "p") {
parser.dump_command();
@ -442,6 +507,7 @@ int main(int argc, char **argv)
if(histfile[0])
write_history(histfile);
s.saveConfig();
return 0;
}

213
shell/p.cpp Normal file
View File

@ -0,0 +1,213 @@
#include "shell.h"
#include "parser.h"
#include "commands.h"
#include <fxos/project.h>
#include <fxos/util/log.h>
#include <fmt/core.h>
//---
// pn
//---
struct _pn_args
{
std::string name = "";
std::string path = "";
};
static struct _pn_args parse_pn(Session &, Parser &parser)
{
_pn_args args;
if(!parser.at_end())
args.name = parser.symbol("project");
if(!parser.at_end())
args.path = parser.str();
parser.end();
return args;
}
void _pn(Session &session, std::string const &name, std::string const &path)
{
if(!session.confirmProjectUnload())
return;
session.switchToNewProject(name, path);
if(path != "") {
session.project()->save();
session.recentProjects().touch(*session.project());
}
}
//---
// pr
//---
static std::string parse_pr(Session &, Parser &p)
{
std::string sym = p.symbol("");
p.end();
return sym;
}
void _pr(Session &session, std::string const &new_name)
{
Project *p = session.project();
if(!p)
return;
p->setName(new_name);
p->setDirty();
}
//---
// ps
//---
static std::string parse_ps(Session &, Parser &p)
{
std::string path;
if(!p.at_end())
path = p.str();
p.end();
return path;
}
void _ps(Session &session, std::string const &new_path)
{
Project *p = session.project();
if(!p) {
FxOS_log(ERR, "No current project o(x_x)o");
return;
}
if(new_path == "" && !p->canSave()) {
FxOS_log(ERR, "Project “%s” has no path; use ps with a path.",
p->name().c_str());
}
if(new_path != "")
p->setPath(new_path);
if(!p->save())
return;
session.recentProjects().touch(*p);
}
//---
// pm
//---
static std::string parse_pm(Session &, Parser &p)
{
std::string sym = p.symbol("vspace_name");
p.end();
return sym;
}
void _pm(Session &session, std::string const &legacy_vspace_name)
{
// TODO: pm
fmt::print("TODO: migrate {} to current project", legacy_vspace_name);
}
//---
// pl
//---
struct _pl_args
{
bool isRecent;
std::string source;
};
static struct _pl_args parse_pl(Session &, Parser &p)
{
struct _pl_args args;
if(p.lookahead().type == T::STRING) {
args.isRecent = false;
args.source = p.str();
}
else {
args.isRecent = true;
args.source = p.symbol("project");
}
p.end();
return args;
}
void _pl(Session &session, bool isRecent, std::string const &source)
{
if(!session.confirmProjectUnload())
return;
if(isRecent) {
std::string path = session.recentProjects().pathFromName(source);
if(path == "") {
FxOS_log(
ERR, "No recent project named “%s”; see ip.", source.c_str());
return;
}
if(session.loadProject(path))
session.recentProjects().touch(*session.project());
}
else {
if(session.loadProject(source))
session.recentProjects().touch(*session.project());
}
}
static ShellCommand _pn_cmd(
"pn",
[](Session &s, Parser &p) {
auto args = parse_pn(s, p);
_pn(s, args.name, args.path);
},
[](Session &s, Parser &p) { parse_pn(s, p); }, "Project New", R"(
pn [<name> ["<path>"]]
Switch a new empty project. If name is not specified, a default is used. If
path is not specified, it will need to be set in the first call to ps when
attempting to save.
)");
static ShellCommand _pr_cmd(
"pr", [](Session &s, Parser &p) { _pr(s, parse_pr(s, p)); },
[](Session &s, Parser &p) { parse_pr(s, p); }, "Project Rename", R"(
pr <name>
Changes the display name of the current troject.
)");
static ShellCommand _ps_cmd(
"ps", [](Session &s, Parser &p) { _ps(s, parse_ps(s, p)); },
[](Session &s, Parser &p) { parse_ps(s, p); }, "Project Save", R"(
ps ["<path>"]
Save the current project. If a folder is specified, save as a new folder.
)");
static ShellCommand _pm_cmd(
"pm", [](Session &s, Parser &p) { _pm(s, parse_pm(s, p)); },
[](Session &s, Parser &p) { parse_pm(s, p); }, "Project Migrate", R"(
pm <vspace_name>
Migrates a legacy virtual space from before version 1.x into the current
project as a new binary. Legacy virtual spaces can be listed with ip.
)");
static ShellCommand _pl_cmd(
"pl",
[](Session &s, Parser &p) {
auto args = parse_pl(s, p);
_pl(s, args.isRecent, args.source);
},
[](Session &s, Parser &p) { parse_pl(s, p); }, "Project Load", R"(
pl (<name> | "<path>")
Load a recent project by name or load a project from a folder.
)");

View File

@ -2,13 +2,102 @@
#include "parser.h"
#include "errors.h"
#include <fmt/core.h>
#include <fxos/util/log.h>
#include <fxos/util/bson.h>
Session::Session(): spaces {}
{
this->current_space = nullptr;
this->pc = -1;
}
void Session::loadConfig(fs::path const &configFile)
{
m_config = configFile;
BSON v = BSON::loadDocumentFromFile(m_config, true, false, "Session");
if(!v.isNull()) {
if(v.hasField("recent"))
m_recent.deserialize(v["recent"]);
}
}
void Session::saveConfig() const
{
if(m_config == "") {
FxOS_log(WRN, "session cannot be saved as there is no config file");
return;
}
fs::create_directories(m_config.parent_path());
FILE *fp = fopen(m_config.c_str(), "w");
if(!fp) {
FxOS_log(ERR, "cannot open '%s': %m", m_config.c_str());
return;
}
BSON b = BSON::mkDocument({
{"*", BSON::mkString("Session")},
{"recent", m_recent.serialize()},
});
b.serialize(fp);
fclose(fp);
}
bool Session::isProjectDirty() const
{
Project *p = m_project.get();
return p && p->isDirty();
}
bool Session::confirmProjectUnload() const
{
if(!m_project || !isProjectDirty())
return true;
fmt::print("Project “{}” is not saved. Discard changes (y/N)? ",
m_project->name());
char *line = NULL;
size_t n = 0;
getline(&line, &n, stdin);
n = strlen(line);
if(n > 0 && line[n - 1] == '\n')
line[n - 1] = '\0';
bool discard = line && (!strcmp(line, "y") || !strcmp(line, "Y"));
free(line);
return discard;
}
void Session::switchToNewProject(
std::string const &name, std::string const &path)
{
auto p = std::make_unique<Project>();
if(name != "")
p->setName(name);
if(path != "")
p->setPath(path);
/* Unload current project - any unsaved data is lost. */
m_project.reset();
m_project = std::move(p);
}
bool Session::loadProject(std::string const &path)
{
auto p = std::make_unique<Project>();
if(!p->load(path))
return false;
/* Unload current project - any unsaved data is lost. */
m_project.reset();
m_project = std::move(p);
return true;
}
//---
VirtualSpace *Session::get_space(std::string name)
{
auto const &it = this->spaces.find(name);

View File

@ -6,8 +6,10 @@
#define FXOS_SESSION_H
#include <fxos/vspace.h>
#include <fxos/project.h>
#include <map>
#include <memory>
#include <string>
#include <filesystem>
@ -16,12 +18,16 @@ namespace fs = std::filesystem;
struct Session
{
/* Empty session with a single empty virtual space */
/* Empty session with an empty project. */
Session();
//---
// Environment
//---
/* Load a configuration file (no error if it doesn't exist). */
void loadConfig(fs::path const &configFile);
/* Save the session info into the configuration file, if one was loaded.
This does not save projects, see Project::save() or the ps command. */
void saveConfig() const;
//=== Environment ===//
/* Search path, folders from FXOS_LIBRARY essentially */
std::vector<fs::path> path;
@ -29,8 +35,46 @@ struct Session
/* Find file by name by searching through the path */
fs::path file(std::string name);
//=== Projects ===//
/* Get the current project. This can only be null during startup and while
switching projects. It is an application invariant that there is always
a project open (even if temporary/unsaved). */
Project *project()
{
return m_project.get();
}
/* Whether the current project is open. This can be used to show warnings
when attempting to close the program or load another project. */
bool isProjectDirty() const;
/* Ask for confirmation to discard unsaved project data if the project is
currently dirty. Returns true if the project is either currently saved,
or the user opts to discard unsaved data. This function does not itself
save the project. */
bool confirmProjectUnload() const;
/* Replace the current project with a fresh one. Any unsaved data is lost;
check isProjectDirty() before calling. */
void switchToNewProject(std::string const &name = "",
std::string const &path = "");
/* Replace the current project by loading an existing one. On success,
unloads the current project; any unsaved data will be lost. Check
isProjectDirty() before calling. On error, returns false and does *not*
unload the current project. */
bool loadProject(std::string const &path);
/* Recent projects. */
RecentProjects &recentProjects()
{
return m_recent;
}
//---
// Virtual spaces
// TODO: To be replaced with legacy descriptions
//---
/* Virtual spaces organized by name */
@ -46,12 +90,13 @@ struct Session
std::string generate_space_name(std::string prefix,
bool force_suffix=false);
//---
//
//---
/* Current cursor location */
uint32_t pc;
private:
/* Path to configuration file. */
fs::path m_config;
/* List of recent projects. */
RecentProjects m_recent;
/* Current project. */
std::unique_ptr<Project> m_project;
};
#endif /* FXOS_SESSION_H */

View File

@ -156,7 +156,7 @@ void _vm(Session &session, std::string file, std::vector<MemoryRegion> regions)
/* If no files are loaded yet, set the PC to the first loaded region */
if(!session.current_space->bindings.size())
session.pc = regions[0].start;
session.current_space->cursor = regions[0].start;
for(auto &r: regions)
session.current_space->bind_region(r, contents);