From 267d2477de910bde833e63817e1f1457c2ce462d Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sun, 21 Sep 2025 23:52:36 +0100 Subject: [PATCH] [Add] Implemented Copy-on-Write and scriptable rm. OK, this commit finally implements the Copy-on-Write architecure I spoke about in previous commits.. It also refactors command execution to be safer and more extensible. To enable CoW and centralise state-changing, command scripts no longer modify the VFS directly. Instead, they return a table describing their intended action '{ action = "rm", target = "file.txt" }'. The C++ CommandProcessor is then responsible for interpreting this and executing it safely. --- common/src/command_processor.cpp | 66 +++++++++++++++++++++++++++++++- common/src/command_processor.h | 1 + common/src/lua_processor.cpp | 41 +++++--------------- common/src/lua_processor.h | 2 +- common/src/vfs_manager.cpp | 20 +++++++++- common/src/vfs_manager.h | 3 ++ 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/common/src/command_processor.cpp b/common/src/command_processor.cpp index 97e5a10..e77c02b 100644 --- a/common/src/command_processor.cpp +++ b/common/src/command_processor.cpp @@ -1,7 +1,9 @@ #include "command_processor.h" +#include #include #include "vfs.h" #include "lua_processor.h" +#include "vfs_manager.h" CommandProcessor::CommandProcessor(vfs_node* starting_dir) { _current_dir = starting_dir; @@ -42,7 +44,13 @@ std::string CommandProcessor::process_command(const std::string& command) { } if(root->children.count("bin") && root->children["bin"]->children.count(script_filename)) { vfs_node* script_node = root->children["bin"]->children[script_filename]; - return _lua->execute(script_node->content, _current_dir, args); + sol::object result = _lua->execute(script_node->content, _current_dir, args); + if(result.is()) { + return result.as(); + } else if(result.is()) { + return _handle_vfs_action(result.as()); + } + return "[Script returned an unexpected type]"; } @@ -68,3 +76,59 @@ std::string CommandProcessor::process_command(const std::string& command) { return "Unknown command: " + command + "\n"; } + +std::string CommandProcessor::_handle_vfs_action(sol::table action) { + std::string action_name = action["action"].get_or(""); + if(action_name == "rm") { + std::string path = action["target"].get_or(""); + if(path.empty()) return "rm: missing operand"; + + /* + * std::map::count was failing weirdly.. + * Instead, iterate manually to find the node. + */ + vfs_node* target_node = nullptr; + for(auto const& [key, node] : _current_dir->children) { + if(key == path) { + target_node = node; + break; + } + } + + if(target_node != nullptr) { + if(_current_dir->read_only) { + /* === Copy on Write === */ + fprintf(stderr, "CoW: Write attempt on read-only dir '%s'. " + "Creating private copy.\n", + _current_dir->name.c_str()); + + vfs_node* grand_parent = _current_dir->parent; + if(!grand_parent) { + return "rm: Permission denied. Cannot modify the root filesystem."; + } + + /* Copy the directory. */ + vfs_node* private_copy = copy_vfs_node(_current_dir, grand_parent); + + /* Link the new private copy into the VFS tree. */ + grand_parent->children[_current_dir->name] = private_copy; + + /* Update our own state to point to the new writable dir. */ + _current_dir = private_copy; + + /* Find the target node again within our new private copy. */ + target_node = _current_dir->children[path]; + } + /* Go ahead and delete, you have a writable target now :) */ + if(target_node->type == DIR_NODE) { + return std::string("rm: cannot remove '") + path + "': Is a directory."; + } + _current_dir->children.erase(path); + delete target_node; + return ""; /* Success. */ + } + return std::string("rm: cannot remove '") + path + "': No such file or directory."; + } + return "Error: Unknown VFS action '" + action_name + "'"; +} + diff --git a/common/src/command_processor.h b/common/src/command_processor.h index 0bf7246..29baf3a 100644 --- a/common/src/command_processor.h +++ b/common/src/command_processor.h @@ -14,6 +14,7 @@ public: vfs_node* get_current_dir(void); private: + std::string _handle_vfs_action(sol::table action); vfs_node* _current_dir; LuaProcessor* _lua; }; diff --git a/common/src/lua_processor.cpp b/common/src/lua_processor.cpp index 109a4f6..19f8f3b 100644 --- a/common/src/lua_processor.cpp +++ b/common/src/lua_processor.cpp @@ -1,4 +1,5 @@ #include "lua_processor.h" +#include #include "vfs.h" LuaProcessor::LuaProcessor(void) { @@ -9,51 +10,27 @@ LuaProcessor::LuaProcessor(void) { "name", &vfs_node::name, "type", &vfs_node::type, "children", &vfs_node::children, - "read_only", &vfs_node::read_only); - - /* Create the 'vfs' table for our API. */ - sol::table vfs_api = _lua.create_table("vfs"); - vfs_api.set_function("rm", [](std::string path, vfs_node* current_dir) { - /* for now, we'll just support deleting from the current directory. */ - if(current_dir->children.count(path)) { - vfs_node* target_node = current_dir->children[path]; - if(target_node->read_only || current_dir->read_only) { - return std::string("rm: Permission denied. File or directory is read-only."); - } - if(target_node->type == DIR_NODE) { - return std::string("rm: cannot remove '" + path + "': Is a directory."); - } - - current_dir->children.erase(path); - delete target_node; /* Clean up memry. */ - return std::string(""); /* Success. */ - } - return std::string("rm: cannot remove '" + path + "': No such file or directory."); - }); -} + "read_only", &vfs_node::read_only); } LuaProcessor::~LuaProcessor(void) {} -std::string LuaProcessor::execute(const std::string& script, vfs_node* current_dir, +sol::object LuaProcessor::execute(const std::string& script, vfs_node* current_dir, const std::vector& args) { try { /* Pass C++ objects/points into the Lua env. */ _lua["current_dir"] = current_dir; /* Create and populate the 'arg' table for the script. */ - sol::table arg_table = _lua.create_table();;;; + sol::table arg_table = _lua.create_table(); for(size_t i = 0; i < args.size(); ++i) { - arg_table[i+1] = args[i]; /* Lua array are 1-indexed. Not that Skuzzy would know. */ + arg_table[i+1] = args[i]; /* Lua arrays 1-indexed. */ } _lua["arg"] = arg_table; - sol::object result = _lua.script(script); - if(result.is()) { - return result.as(); - } + return _lua.script(script); + } catch(const sol::error& e) { - return e.what(); + /* Return the error message as a string in a sol::object. */ + return sol::make_object(_lua, e.what()); } - /* Shouldn't reach this, just shutting the compiler up. */ - return "[Unknown error in LuaProcessor::execute]"; } diff --git a/common/src/lua_processor.h b/common/src/lua_processor.h index bb88f37..e3dddc2 100644 --- a/common/src/lua_processor.h +++ b/common/src/lua_processor.h @@ -11,7 +11,7 @@ public: ~LuaProcessor(void); /* Executes a string of lua code and returns result as a string. */ - std::string execute(const std::string& script, vfs_node* current_dir, + sol::object execute(const std::string& script, vfs_node* current_dir, const std::vector& args); private: sol::state _lua; diff --git a/common/src/vfs_manager.cpp b/common/src/vfs_manager.cpp index 241fc32..a630b64 100644 --- a/common/src/vfs_manager.cpp +++ b/common/src/vfs_manager.cpp @@ -11,6 +11,24 @@ vfs_node* new_node(std::string name, vfs_node_type type, vfs_node* parent) { return node; } +vfs_node* copy_vfs_node(vfs_node* original, vfs_node* new_parent) { + if(!original) { + return nullptr; + } + + /* Create the new node and copy its properties. */ + vfs_node* new_copy = new_node(original->name, original->type, new_parent); + new_copy->read_only = false; /* The new copy is *always* writeable. */ + new_copy->content = original->content; + + /* Recursively copy all children. */ + for(auto const& [key, child_node] : original->children) { + new_copy->children[key] = copy_vfs_node(child_node, new_copy); + } + + return new_copy; +} + VFSManager::VFSManager(void) { /* Create template VFS that holds shared, read-only directories. */ _vfs_root = new_node("/", DIR_NODE, nullptr); @@ -46,7 +64,7 @@ VFSManager::VFSManager(void) { rm_script->content = R"lua(-- /bin/rm.lua - Removes a file. local file_to_remove = arg[1] if not file_to_remove then return "rm: missing operand" end - return vfs.rm(file_to_remove, current_dir) + return { action = "rm", target = file_to_remove } )lua"; bin->children["rm.lua"] = rm_script; } diff --git a/common/src/vfs_manager.h b/common/src/vfs_manager.h index ec328e1..9f8b8fd 100644 --- a/common/src/vfs_manager.h +++ b/common/src/vfs_manager.h @@ -4,6 +4,9 @@ #include "vfs.h" +/* Recursive copy function for our Copy-on-Write behaviour. */ +vfs_node* copy_vfs_node(vfs_node* original, vfs_node* new_parent); + class VFSManager { public: VFSManager(void);