diff --git a/assets/scripts/bin/echo.lua b/assets/scripts/bin/echo.lua new file mode 100644 index 0000000..9c5127d --- /dev/null +++ b/assets/scripts/bin/echo.lua @@ -0,0 +1,25 @@ +-- /bin/echo - Display contents of a text file. +local content_parts = {} +local filename = nil +local found_redirect = false + +for i, v in ipairs(arg) do + if v == ">" then + found_redirect = true + filename = arg[i+1] + break + else + table.insert(content_parts, v) + end +end + +if found_redirect then + if not filename then + return "echo: syntax error: expected filename after '>'" + end + local content = table.concat(content_parts, " ") + return { action = "write_file", target = filename, content = content } +else + return table.concat(arg, " ") +end + diff --git a/common/src/command_processor.cpp b/common/src/command_processor.cpp index cc1b444..4ac7446 100644 --- a/common/src/command_processor.cpp +++ b/common/src/command_processor.cpp @@ -1,4 +1,5 @@ #include "command_processor.h" +#include #include #include "vfs.h" @@ -60,54 +61,106 @@ std::string CommandProcessor::process_command(const std::string& command) { return "Unknown command: " + command_name + "\n"; } +/* Find a VFS node by it's absolute path. */ +vfs_node* find_node_by_path(vfs_node* root, const std::string& path) { + if(path == "/") { + return root; + } + vfs_node* current = root; + std::stringstream ss(path); + std::string segment; + + /* Discard the first empty segment that comes form the leading '/'. */ + if(path[0] == '/') { + std::getline(ss, segment, '/'); + } + + while(std::getline(ss, segment, '/')) { + if(current->type == DIR_NODE && current->children.count(segment)) { + current = current->children[segment]; + } else { + return nullptr; /* Path segmenet not found. */ + } + } + return current; +} + +/* Recursively set all nodes in a VFS tree to writeable. */ +void make_vfs_writable(vfs_node* node) { + if(!node) return; + node->read_only = false; + for(auto const& [key, child] : node->children) { + make_vfs_writable(child); + } +} + std::string CommandProcessor::_handle_vfs_action(sol::table action) { std::string action_name = action["action"].get_or(""); + /* Make the CoW check universal for any write operation. */ + if(action_name == "rm" || action_name == "write_file") { + if(_current_dir->read_only) { + std::string remote_ip = ""; + /* Check if we are on a known remote system. */ + if(session_vfs_root != home_vfs_root) { + for(auto const& [ip, root_node] : _world_vfs) { + if(root_node == session_vfs_root) { + remote_ip = ip; + break; + } + } + } + + if(!remote_ip.empty()) { + /* CoW for a remote system. */ + fprintf(stderr, "CoW: Write attempt on read-only remote system '%s'." + "Creating persistant copy.\n", remote_ip.c_str()); + std::string original_path = get_full_path(_current_dir); + vfs_node* new_root = copy_vfs_node(session_vfs_root, nullptr); + make_vfs_writable(new_root); + _world_vfs[remote_ip] = new_root; + session_vfs_root = new_root; + _current_dir = find_node_by_path(session_vfs_root, original_path); + if(!_current_dir) { _current_dir = session_vfs_root; } /* Just in case. */ + } else { + /* If not a known remote system, i.e, player's own /bin, deny. */ + return "Permission denied: Filesystem is read-only."; + } + } + } 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. */ - } + if(!_current_dir->children.count(path)) { return std::string("rm: cannot remove '") + path + "': No such file or directory."; + } + vfs_node* target_node = _current_dir->children[path]; + if(target_node->type == DIR_NODE) { + return std::string("rm: cannot remove '") + path + "': Is a directory."; + } + _current_dir->children.erase(path); + delete target_node; + } else if(action_name == "write_file") { + std::string filename = action["target"].get_or(""); + std::string content = action["content"].get_or(""); + if(filename.empty()) { + return "write_file: missing filename"; + } + + if(_current_dir->children.count(filename)) { + vfs_node* target_node = _current_dir->children[filename]; + if(target_node->type == DIR_NODE) { + return std::string("cannot write to '") + filename + "': Is a directory."; + } + target_node->content = content; + } else { + vfs_node* new_file = new vfs_node { + .name = filename, .type = FILE_NODE, .read_only = false, .content = content, + .parent = _current_dir + }; + _current_dir->children[filename] = new_file; + } + return ""; /* Success. */ } else if(action_name == "ssh") { std::string target_ip = action["target"].get_or(""); if(_world_vfs.count(target_ip)) { diff --git a/common/src/lua_processor.cpp b/common/src/lua_processor.cpp index cc587d6..4fbff64 100644 --- a/common/src/lua_processor.cpp +++ b/common/src/lua_processor.cpp @@ -5,7 +5,7 @@ #include "vfs.h" LuaProcessor::LuaProcessor(void) { - _lua.open_libraries(sol::lib::base, sol::lib::string, sol::lib::io); + _lua.open_libraries(sol::lib::base, sol::lib::string, sol::lib::io, sol::lib::table); /* Expose vfs_node struct members to Lua. */ _lua.new_usertype("vfs_node", diff --git a/common/src/vfs_manager.cpp b/common/src/vfs_manager.cpp index 17d61e7..c5e620c 100644 --- a/common/src/vfs_manager.cpp +++ b/common/src/vfs_manager.cpp @@ -22,7 +22,7 @@ vfs_node* copy_vfs_node(vfs_node* original, vfs_node* new_parent) { /* 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->read_only = original->read_only; /* Preserver read-only status. */ new_copy->content = original->content; /* Recursively copy all children. */