| // Copyright 2022 The Emscripten Authors. All rights reserved. |
| // Emscripten is available under two separate licenses, the MIT license and the |
| // University of Illinois/NCSA Open Source License. Both these licenses can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| |
| #include "backend.h" |
| #include "file.h" |
| #include "node_backend.h" |
| #include "support.h" |
| #include "wasmfs.h" |
| |
| namespace wasmfs { |
| |
| class NodeBackend; |
| |
| // The state of a file on the underlying Node file system. |
| class NodeState { |
| // Map all separate WasmFS opens of a file to a single underlying fd. |
| size_t openCount = 0; |
| oflags_t openFlags = 0; |
| int fd = -1; |
| |
| public: |
| std::string path; |
| |
| NodeState(std::string path) : path(path) {} |
| |
| int getFD() { |
| assert(openCount > 0); |
| return fd; |
| } |
| |
| bool isOpen() { return openCount > 0; } |
| |
| // Attempt to open the file with the given flags, returning 0 on success or an |
| // error code. |
| int open(oflags_t flags) { |
| int result = 0; |
| if (openCount == 0) { |
| // No existing fd, so open a fresh one. |
| switch (flags) { |
| case O_RDONLY: |
| result = _wasmfs_node_open(path.c_str(), "r"); |
| break; |
| case O_WRONLY: |
| // TODO(sbc): Specific handling of O_WRONLY. |
| // There is no simple way to map O_WRONLY to an fopen-style |
| // mode string since the only two modes that are write only |
| // are `w` and `a`. The problem with the former is that it |
| // truncates to file. The problem with the latter is that it |
| // opens for appending. For now simply opening in O_RDWR |
| // mode is enough to pass all our tests. |
| case O_RDWR: |
| result = _wasmfs_node_open(path.c_str(), "r+"); |
| break; |
| default: |
| WASMFS_UNREACHABLE("Unexpected open access mode"); |
| } |
| if (result < 0) { |
| return result; |
| } |
| // Fall through to update our state with the new result. |
| } else if ((openFlags == O_RDONLY && |
| (flags == O_WRONLY || flags == O_RDWR)) || |
| (openFlags == O_WRONLY && |
| (flags == O_RDONLY || flags == O_RDWR))) { |
| // We already have a file descriptor, but we need to replace it with a new |
| // fd with more access. |
| result = _wasmfs_node_open(path.c_str(), "r+"); |
| if (result < 0) { |
| return result; |
| } |
| // Success! Close the old fd before updating it. |
| (void)_wasmfs_node_close(fd); |
| // Fall through to update our state with the new result. |
| } else { |
| // Reuse the existing file descriptor. |
| ++openCount; |
| return 0; |
| } |
| // Update our state for the new fd. |
| fd = result; |
| openFlags = flags; |
| ++openCount; |
| return 0; |
| } |
| |
| int close() { |
| int ret = 0; |
| if (--openCount == 0) { |
| ret = _wasmfs_node_close(fd); |
| *this = NodeState(path); |
| } |
| return ret; |
| } |
| }; |
| |
| class NodeFile : public DataFile { |
| public: |
| NodeState state; |
| |
| NodeFile(mode_t mode, backend_t backend, std::string path) |
| : DataFile(mode, backend), state(path) {} |
| |
| private: |
| off_t getSize() override { |
| // TODO: This should really be using a 64-bit file size type. |
| uint32_t size; |
| if (state.isOpen()) { |
| if (_wasmfs_node_fstat_size(state.getFD(), &size)) { |
| // TODO: Make this fallible. |
| return 0; |
| } |
| } else { |
| if (_wasmfs_node_stat_size(state.path.c_str(), &size)) { |
| // TODO: Make this fallible. |
| return 0; |
| } |
| } |
| return off_t(size); |
| } |
| |
| int setSize(off_t size) override { |
| if (state.isOpen()) { |
| return -_wasmfs_node_ftruncate(state.getFD(), size); |
| } |
| return -_wasmfs_node_truncate(state.path.c_str(), size); |
| } |
| |
| int open(oflags_t flags) override { return state.open(flags); } |
| |
| int close() override { return state.close(); } |
| |
| ssize_t read(uint8_t* buf, size_t len, off_t offset) override { |
| uint32_t nread; |
| if (auto err = _wasmfs_node_read(state.getFD(), buf, len, offset, &nread)) { |
| return -err; |
| } |
| return nread; |
| } |
| |
| ssize_t write(const uint8_t* buf, size_t len, off_t offset) override { |
| uint32_t nwritten; |
| if (auto err = |
| _wasmfs_node_write(state.getFD(), buf, len, offset, &nwritten)) { |
| return -err; |
| } |
| return nwritten; |
| } |
| |
| int flush() override { |
| WASMFS_UNREACHABLE("TODO: implement NodeFile::flush"); |
| } |
| }; |
| |
| class NodeSymlink : public Symlink { |
| public: |
| std::string path; |
| |
| NodeSymlink(backend_t backend, std::string path) |
| : Symlink(backend), path(path) {} |
| |
| virtual std::string getTarget() const { |
| char buf[PATH_MAX]; |
| if (_wasmfs_node_readlink(path.c_str(), buf, PATH_MAX) < 0) { |
| WASMFS_UNREACHABLE("getTarget cannot fail"); |
| } |
| return std::string(buf); |
| } |
| }; |
| |
| class NodeDirectory : public Directory { |
| public: |
| NodeState state; |
| |
| NodeDirectory(mode_t mode, backend_t backend, std::string path) |
| : Directory(mode, backend), state(path) {} |
| |
| private: |
| std::string getChildPath(const std::string& name) { |
| return state.path + '/' + name; |
| } |
| |
| std::shared_ptr<File> getChild(const std::string& name) override { |
| static_assert(std::is_same_v<mode_t, unsigned int>); |
| // TODO: also retrieve and set ctime, atime, ino, etc. |
| auto childPath = getChildPath(name); |
| mode_t mode; |
| if (_wasmfs_node_get_mode(childPath.c_str(), &mode)) { |
| return nullptr; |
| } |
| // Allow reading from character device files too (e.g. `/dev/random`, |
| // `/dev/urandom`) |
| if (S_ISREG(mode) || S_ISCHR(mode)) { |
| return std::make_shared<NodeFile>(mode, getBackend(), childPath); |
| } else if (S_ISDIR(mode)) { |
| return std::make_shared<NodeDirectory>(mode, getBackend(), childPath); |
| } else if (S_ISLNK(mode)) { |
| return std::make_shared<NodeSymlink>(getBackend(), childPath); |
| } else { |
| // Unrecognized file kind not made visible to WasmFS. |
| return nullptr; |
| } |
| } |
| |
| int removeChild(const std::string& name) override { |
| auto childPath = getChildPath(name); |
| // Try both `unlink` and `rmdir`. |
| if (auto err = _wasmfs_node_unlink(childPath.c_str())) { |
| if (err == EISDIR) { |
| err = _wasmfs_node_rmdir(childPath.c_str()); |
| } |
| return -err; |
| } |
| return 0; |
| } |
| |
| std::shared_ptr<DataFile> insertDataFile(const std::string& name, |
| mode_t mode) override { |
| auto childPath = getChildPath(name); |
| if (_wasmfs_node_insert_file(childPath.c_str(), mode)) { |
| return nullptr; |
| } |
| return std::make_shared<NodeFile>(mode, getBackend(), childPath); |
| } |
| |
| std::shared_ptr<Directory> insertDirectory(const std::string& name, |
| mode_t mode) override { |
| auto childPath = getChildPath(name); |
| if (_wasmfs_node_insert_directory(childPath.c_str(), mode)) { |
| return nullptr; |
| } |
| return std::make_shared<NodeDirectory>(mode, getBackend(), childPath); |
| } |
| |
| std::shared_ptr<Symlink> insertSymlink(const std::string& name, |
| const std::string& target) override { |
| auto childPath = getChildPath(name); |
| if (_wasmfs_node_symlink(target.c_str(), childPath.c_str())) { |
| return nullptr; |
| } |
| return std::make_shared<NodeSymlink>(getBackend(), childPath); |
| } |
| |
| int insertMove(const std::string& name, std::shared_ptr<File> file) override { |
| std::string fromPath; |
| |
| if (file->is<DataFile>()) { |
| auto nodeFile = std::static_pointer_cast<NodeFile>(file); |
| fromPath = nodeFile->state.path; |
| } else { |
| auto nodeDir = std::static_pointer_cast<NodeDirectory>(file); |
| fromPath = nodeDir->state.path; |
| } |
| |
| auto childPath = getChildPath(name); |
| return -_wasmfs_node_rename(fromPath.c_str(), childPath.c_str()); |
| } |
| |
| ssize_t getNumEntries() override { |
| // TODO: optimize this? |
| auto entries = getEntries(); |
| if (int err = entries.getError()) { |
| return err; |
| } |
| return entries->size(); |
| } |
| |
| Directory::MaybeEntries getEntries() override { |
| std::vector<Directory::Entry> entries; |
| int err = _wasmfs_node_readdir(state.path.c_str(), &entries); |
| if (err) { |
| return {-err}; |
| } |
| return {entries}; |
| } |
| }; |
| |
| class NodeBackend : public Backend { |
| // The underlying Node FS path of this backend's mount points. |
| std::string mountPath; |
| |
| public: |
| NodeBackend(const std::string& mountPath) : mountPath(mountPath) {} |
| |
| std::shared_ptr<DataFile> createFile(mode_t mode) override { |
| return std::make_shared<NodeFile>(mode, this, mountPath); |
| } |
| |
| std::shared_ptr<Directory> createDirectory(mode_t mode) override { |
| return std::make_shared<NodeDirectory>(mode, this, mountPath); |
| } |
| |
| std::shared_ptr<Symlink> createSymlink(std::string target) override { |
| WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); |
| } |
| }; |
| |
| // TODO: symlink |
| |
| extern "C" { |
| |
| backend_t wasmfs_create_node_backend(const char* root) { |
| return wasmFS.addBackend(std::make_unique<NodeBackend>(root)); |
| } |
| |
| void EMSCRIPTEN_KEEPALIVE _wasmfs_node_record_dirent( |
| std::vector<Directory::Entry>* entries, const char* name, int type) { |
| entries->push_back({name, File::FileKind(type), 0}); |
| } |
| |
| } // extern "C" |
| |
| } // namespace wasmfs |