280 строки
9.5 KiB
C++
280 строки
9.5 KiB
C++
// Copyright 2021 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.
|
|
|
|
// This file defines the file object of the new file system.
|
|
// Current Status: Work in Progress.
|
|
// See https://github.com/emscripten-core/emscripten/issues/15041.
|
|
|
|
#pragma once
|
|
|
|
#include <assert.h>
|
|
#include <emscripten/html5.h>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <sys/stat.h>
|
|
#include <vector>
|
|
#include <wasi/api.h>
|
|
|
|
namespace wasmfs {
|
|
|
|
// Note: The general locking strategy for all Files is to only hold 1 lock at a
|
|
// time to prevent deadlock. This methodology can be seen in getDirs().
|
|
|
|
class Backend;
|
|
// This represents an opaque pointer to a Backend. A user may use this to
|
|
// specify a backend in file operations.
|
|
using backend_t = Backend*;
|
|
const backend_t NullBackend = nullptr;
|
|
|
|
class File : public std::enable_shared_from_this<File> {
|
|
|
|
public:
|
|
enum FileKind { DataFileKind = 0, DirectoryKind, SymlinkKind };
|
|
|
|
template<class T> bool is() const {
|
|
static_assert(std::is_base_of<File, T>::value,
|
|
"File is not a base of destination type T");
|
|
return int(kind) == int(T::expectedKind);
|
|
}
|
|
|
|
template<class T> std::shared_ptr<T> dynCast() {
|
|
static_assert(std::is_base_of<File, T>::value,
|
|
"File is not a base of destination type T");
|
|
if (int(kind) == int(T::expectedKind)) {
|
|
return std::static_pointer_cast<T>(shared_from_this());
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
template<class T> std::shared_ptr<T> cast() {
|
|
static_assert(std::is_base_of<File, T>::value,
|
|
"File is not a base of destination type T");
|
|
assert(int(kind) == int(T::expectedKind));
|
|
return std::static_pointer_cast<T>(shared_from_this());
|
|
}
|
|
|
|
ino_t getIno() {
|
|
// Set inode number to the file pointer. This gives a unique inode number.
|
|
// TODO: For security it would be better to use an indirect mapping.
|
|
// Ensure that the pointer will not overflow an ino_t.
|
|
static_assert(sizeof(this) <= sizeof(ino_t));
|
|
return (ino_t)this;
|
|
}
|
|
|
|
backend_t getBackend() { return backend; }
|
|
|
|
class Handle {
|
|
|
|
protected:
|
|
// This mutex is needed when one needs to access access a previously locked
|
|
// file in the same thread. For example, rename will need to traverse
|
|
// 2 paths and access the same locked directory twice.
|
|
// TODO: During benchmarking, test recursive vs normal mutex performance.
|
|
std::unique_lock<std::recursive_mutex> lock;
|
|
std::shared_ptr<File> file;
|
|
|
|
public:
|
|
Handle(std::shared_ptr<File> file) : file(file), lock(file->mutex) {}
|
|
Handle(std::shared_ptr<File> file, std::defer_lock_t)
|
|
: file(file), lock(file->mutex, std::defer_lock) {}
|
|
bool trylock() { return lock.try_lock(); }
|
|
size_t getSize() { return file->getSize(); }
|
|
mode_t& mode() { return file->mode; }
|
|
time_t& ctime() { return file->ctime; }
|
|
time_t& mtime() { return file->mtime; }
|
|
time_t& atime() { return file->atime; }
|
|
|
|
// Note: parent.lock() creates a new shared_ptr to the same Directory
|
|
// specified by the parent weak_ptr.
|
|
std::shared_ptr<File> getParent() { return file->parent.lock(); }
|
|
void setParent(std::shared_ptr<File> parent) { file->parent = parent; }
|
|
};
|
|
|
|
Handle locked() { return Handle(shared_from_this()); }
|
|
|
|
std::optional<Handle> maybeLocked() {
|
|
auto handle = Handle(shared_from_this(), std::defer_lock);
|
|
if (handle.trylock()) {
|
|
return Handle(shared_from_this());
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
protected:
|
|
File(FileKind kind, mode_t mode, backend_t backend)
|
|
: kind(kind), mode(mode), backend(backend) {}
|
|
// A mutex is needed for multiple accesses to the same file.
|
|
std::recursive_mutex mutex;
|
|
|
|
virtual size_t getSize() = 0;
|
|
|
|
mode_t mode = 0; // User and group mode bits for access permission.
|
|
|
|
time_t ctime = 0; // Time when the file node was last modified.
|
|
time_t mtime = 0; // Time when the file content was last modified.
|
|
time_t atime = 0; // Time when the content was last accessed.
|
|
|
|
FileKind kind;
|
|
|
|
// Reference to parent of current file node. This can be used to
|
|
// traverse up the directory tree. A weak_ptr ensures that the ref
|
|
// count is not incremented. This also ensures that there are no cyclic
|
|
// dependencies where the parent and child have shared_ptrs that reference
|
|
// each other. This prevents the case in which an uncollectable cycle occurs.
|
|
std::weak_ptr<File> parent;
|
|
|
|
// This specifies which backend a file is associated with.
|
|
backend_t backend;
|
|
};
|
|
|
|
class DataFile : public File {
|
|
|
|
virtual __wasi_errno_t read(uint8_t* buf, size_t len, off_t offset) = 0;
|
|
virtual __wasi_errno_t
|
|
write(const uint8_t* buf, size_t len, off_t offset) = 0;
|
|
|
|
public:
|
|
static constexpr FileKind expectedKind = File::DataFileKind;
|
|
DataFile(mode_t mode, backend_t backend)
|
|
: File(File::DataFileKind, mode | S_IFREG, backend) {}
|
|
DataFile(mode_t mode, backend_t backend, mode_t fileType)
|
|
: File(File::DataFileKind, mode | fileType, backend) {}
|
|
virtual ~DataFile() = default;
|
|
|
|
class Handle : public File::Handle {
|
|
|
|
std::shared_ptr<DataFile> getFile() { return file->cast<DataFile>(); }
|
|
|
|
public:
|
|
Handle(std::shared_ptr<File> dataFile) : File::Handle(dataFile) {}
|
|
Handle(Handle&&) = default;
|
|
|
|
__wasi_errno_t read(uint8_t* buf, size_t len, off_t offset) {
|
|
return getFile()->read(buf, len, offset);
|
|
}
|
|
__wasi_errno_t write(const uint8_t* buf, size_t len, off_t offset) {
|
|
return getFile()->write(buf, len, offset);
|
|
}
|
|
|
|
// This function loads preloaded files from JS Memory into this DataFile.
|
|
// TODO: Make this virtual so specific backends can specialize it for better performance.
|
|
void preloadFromJS(int index);
|
|
};
|
|
|
|
Handle locked() { return Handle(shared_from_this()); }
|
|
};
|
|
|
|
class Directory : public File {
|
|
protected:
|
|
// TODO: maybe change to vector?
|
|
std::map<std::string, std::shared_ptr<File>> entries;
|
|
// 4096 bytes is the size of a block in ext4.
|
|
// This value was also copied from the existing file system.
|
|
size_t getSize() override { return 4096; }
|
|
|
|
public:
|
|
static constexpr FileKind expectedKind = File::DirectoryKind;
|
|
Directory(mode_t mode, backend_t backend)
|
|
: File(File::DirectoryKind, mode | S_IFDIR, backend) {}
|
|
|
|
struct Entry {
|
|
std::string name;
|
|
std::shared_ptr<File> file;
|
|
};
|
|
|
|
class Handle : public File::Handle {
|
|
std::shared_ptr<Directory> getDir() { return file->cast<Directory>(); }
|
|
|
|
public:
|
|
Handle(std::shared_ptr<File> directory) : File::Handle(directory) {}
|
|
Handle(std::shared_ptr<File> directory, std::defer_lock_t)
|
|
: File::Handle(directory, std::defer_lock) {}
|
|
|
|
std::shared_ptr<File> getEntry(std::string pathName);
|
|
|
|
void setEntry(std::string pathName, std::shared_ptr<File> inserted) {
|
|
// Hold the lock over both functions to cover the case in which two
|
|
// directories attempt to add the file.
|
|
auto lockedInserted = inserted->locked();
|
|
getDir()->entries[pathName] = inserted;
|
|
// Simultaneously, set the parent of the inserted node to be this Dir.
|
|
// inserted must be locked because we have to go through Handle.
|
|
// TODO: When rename is implemented, ensure that the source directory has
|
|
// been removed as a parent.
|
|
// https://github.com/emscripten-core/emscripten/pull/15410#discussion_r742171264
|
|
assert(!lockedInserted.getParent());
|
|
lockedInserted.setParent(file);
|
|
}
|
|
|
|
void unlinkEntry(std::string pathName) {
|
|
// The file lock must be held for both operations. Removing the child file
|
|
// from the parent's entries and removing the parent pointer from the
|
|
// child should be atomic. The state should not be mutated in between.
|
|
auto unlinked = getDir()->entries[pathName]->locked();
|
|
unlinked.setParent({});
|
|
getDir()->entries.erase(pathName);
|
|
}
|
|
|
|
// Used to obtain name of child File in the directory entries vector.
|
|
std::string getName(std::shared_ptr<File> target) {
|
|
for (const auto& [key, value] : getDir()->entries) {
|
|
if (value == target) {
|
|
return key;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
int getNumEntries() { return getDir()->entries.size(); }
|
|
|
|
// Return a vector of the key-value pairs in entries.
|
|
std::vector<Directory::Entry> getEntries() {
|
|
std::vector<Directory::Entry> entries;
|
|
for (const auto& [key, value] : getDir()->entries) {
|
|
entries.push_back({key, value});
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
#ifdef WASMFS_DEBUG
|
|
void printKeys() {
|
|
for (auto keyPair : getDir()->entries) {
|
|
emscripten_console_log(keyPair.first.c_str());
|
|
}
|
|
}
|
|
#endif
|
|
};
|
|
|
|
Handle locked() { return Handle(shared_from_this()); }
|
|
|
|
std::optional<Handle> maybeLocked() {
|
|
auto handle = Handle(shared_from_this(), std::defer_lock);
|
|
if (handle.trylock()) {
|
|
return Handle(shared_from_this());
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
};
|
|
// Obtains parent directory of a given pathname.
|
|
// Will return a nullptr if the parent is not a directory.
|
|
// Will error if the forbiddenAncestor is encountered while processing.
|
|
// If the forbiddenAncestor is encountered, err will be set to EINVAL and
|
|
// nullptr will be returned.
|
|
std::shared_ptr<Directory>
|
|
getDir(std::vector<std::string>::iterator begin,
|
|
std::vector<std::string>::iterator end,
|
|
long& err,
|
|
std::shared_ptr<File> forbiddenAncestor = nullptr);
|
|
|
|
// Return a vector of the '/'-delimited components of a path. The first
|
|
// element will be "/" iff the path is an absolute path.
|
|
std::vector<std::string> splitPath(char* pathname);
|
|
|
|
} // namespace wasmfs
|