blob: 02239861e21e14498ea0272871778f573acd0c30 [file] [edit]
// 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 "support.h"
#include <assert.h>
#include <emscripten/html5.h>
#include <map>
#include <mutex>
#include <optional>
#include <sys/stat.h>
#include <variant>
#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;
class Directory;
class Symlink;
// 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;
// Access mode, file creation and file status flags for open.
using oflags_t = uint32_t;
// An abstract representation of an underlying file. All `File` objects
// correspond to underlying (real or conceptual) files in a file system managed
// by some backend, but not all underlying files have a corresponding `File`
// object. For example, a persistent backend may contain some files that have
// not yet been discovered by WasmFS and that therefore do not yet have
// corresponding `File` objects. Backends override the `File` family of classes
// to implement the mapping from `File` objects to their underlying files.
class File : public std::enable_shared_from_this<File> {
public:
enum FileKind {
UnknownKind = 0,
DataFileKind = 1,
DirectoryKind = 2,
SymlinkKind = 3
};
const FileKind kind;
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() const { return backend; }
bool isSeekable() const { return seekable; }
class Handle;
Handle locked();
protected:
File(FileKind kind, mode_t mode, backend_t backend)
: kind(kind), mode(mode), backend(backend) {
atime = mtime = ctime = time(NULL);
}
// A mutex is needed for multiple accesses to the same file.
std::recursive_mutex mutex;
// The size in bytes of a file or return a negative error code. May be
// called on files that have not been opened.
virtual off_t getSize() = 0;
mode_t mode = 0; // User and group mode bits for access permission.
time_t atime = 0; // Time when the content was last accessed.
time_t mtime = 0; // Time when the file content was last modified.
time_t ctime = 0; // Time when the file node was last modified.
// 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<Directory> parent;
// This specifies which backend a file is associated with. It may be null
// (NullBackend) if there is no particular backend associated with the file.
backend_t backend;
// By default files are seekable. The rare exceptions are things like pipes
// and sockets.
bool seekable = true;
};
class DataFile : public File {
protected:
// Notify the backend when this file is opened or closed. The backend is
// responsible for keeping files accessible as long as they are open, even if
// they are unlinked. Returns 0 on success or a negative error code.
virtual int open(oflags_t flags) = 0;
virtual int close() = 0;
// Return the accessed length or a negative error code. It is not an error to
// access fewer bytes than requested. Will only be called on opened files.
// TODO: Allow backends to override the version of read with
// multiple iovecs to make it possible to implement pipes. See #16269.
virtual ssize_t read(uint8_t* buf, size_t len, off_t offset) = 0;
virtual ssize_t write(const uint8_t* buf, size_t len, off_t offset) = 0;
// Sets the size of the file to a specific size. If new space is allocated, it
// should be zero-initialized. May be called on files that have not been
// opened. Returns 0 on success or a negative error code.
virtual int setSize(off_t size) = 0;
// Sync the file data to the underlying persistent storage, if any. Returns 0
// on success or a negative error code.
virtual int flush() = 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;
Handle locked();
};
class Directory : public File {
public:
struct Entry {
std::string name;
FileKind kind;
ino_t ino;
};
struct MaybeEntries : std::variant<std::vector<Entry>, int> {
int getError() {
if (int* err = std::get_if<int>(this)) {
assert(*err < 0);
return *err;
}
return 0;
}
std::vector<Entry>& operator*() {
return *std::get_if<std::vector<Entry>>(this);
}
std::vector<Entry>* operator->() {
return std::get_if<std::vector<Entry>>(this);
}
};
private:
// The directory cache, or `dcache`, stores `File` objects for the children of
// each directory so that subsequent lookups do not need to query the backend.
// It also supports cross-backend mount point children that are stored
// exclusively in the cache and not reflected in any backend.
enum class DCacheKind { Normal, Mount };
struct DCacheEntry {
DCacheKind kind;
std::shared_ptr<File> file;
};
// TODO: Use a cache data structure with smaller code size.
std::map<std::string, DCacheEntry> dcache;
protected:
// Return the `File` object corresponding to the file with the given name or
// null if there is none.
virtual std::shared_ptr<File> getChild(const std::string& name) = 0;
// Inserts a file with the given name, kind, and mode. Returns a `File` object
// corresponding to the newly created file or nullptr if the new file could
// not be created. Assumes a child with this name does not already exist.
// If the operation failed, returns nullptr.
virtual std::shared_ptr<DataFile> insertDataFile(const std::string& name,
mode_t mode) = 0;
virtual std::shared_ptr<Directory> insertDirectory(const std::string& name,
mode_t mode) = 0;
virtual std::shared_ptr<Symlink> insertSymlink(const std::string& name,
const std::string& target) = 0;
// Move the file represented by `file` from its current directory to this
// directory with the new `name`, possibly overwriting another file that
// already exists with that name. The old directory may be the same as this
// directory. On success return 0 and otherwise return a negative error code
// without changing any underlying state.
virtual int insertMove(const std::string& name,
std::shared_ptr<File> file) = 0;
// Remove the file with the given name. Returns zero on success or if the
// child has already been removed and otherwise returns a negative error code
// if the child cannot be removed.
virtual int removeChild(const std::string& name) = 0;
// The number of entries in this directory. Returns the number of entries or a
// negative error code.
virtual ssize_t getNumEntries() = 0;
// The list of entries in this directory or a negative error code.
virtual MaybeEntries getEntries() = 0;
// Only backends that maintain file identity themselves (see below) need to
// implement this.
virtual std::string getName(std::shared_ptr<File> file) {
WASMFS_UNREACHABLE("getName unimplemented");
}
// Whether this directory implementation always returns the same `File` object
// for a given file. Most backends can be much simpler if they don't handle
// this themselves. Instead, they rely on the directory cache (dcache) to
// maintain file identity for them by ensuring each file is looked up in the
// backend only once. Some backends, however, already track file identity, so
// the dcache is not necessary (or would even introduce problems).
//
// When this is `true`, backends are responsible for:
//
// 1. Ensuring that all insert* and getChild calls returning a particular
// file return the same File object.
//
// 2. Clearing unlinked Files' parents in `removeChild` and `insertMove`.
//
// 3. Implementing `getName`, since it cannot be implemented in terms of the
// dcache.
virtual bool maintainsFileIdentity() { return false; }
public:
static constexpr FileKind expectedKind = File::DirectoryKind;
Directory(mode_t mode, backend_t backend)
: File(File::DirectoryKind, mode | S_IFDIR, backend) {}
virtual ~Directory() = default;
class Handle;
Handle locked();
protected:
// 4096 bytes is the size of a block in ext4.
// This value was also copied from the JS file system.
off_t getSize() override { return 4096; }
};
class Symlink : public File {
public:
static constexpr FileKind expectedKind = File::SymlinkKind;
// Note that symlinks provide a mode of 0 to File. The mode of a symlink does
// not matter, so that value will never be read (what matters is the mode of
// the target).
Symlink(backend_t backend) : File(File::SymlinkKind, S_IFLNK, backend) {}
virtual ~Symlink() = default;
// Constant, and therefore thread-safe, and can be done without locking.
virtual std::string getTarget() const = 0;
protected:
off_t getSize() override { return getTarget().size(); }
};
class File::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) : lock(file->mutex), file(file) {}
Handle(std::shared_ptr<File> file, std::defer_lock_t)
: lock(file->mutex, std::defer_lock), file(file) {}
off_t getSize() { return file->getSize(); }
mode_t getMode() { return file->mode; }
void setMode(mode_t mode) {
// The type bits can never be changed (whether something is a file or a
// directory, for example).
file->mode = (file->mode & S_IFMT) | (mode & ~S_IFMT);
}
time_t getCTime() { return file->ctime; }
void setCTime(time_t time) { file->ctime = time; }
time_t getMTime() { return file->mtime; }
void setMTime(time_t time) { file->mtime = time; }
time_t getATime() { return file->atime; }
void setATime(time_t time) { file->atime = time; }
// Note: parent.lock() creates a new shared_ptr to the same Directory
// specified by the parent weak_ptr.
std::shared_ptr<Directory> getParent() { return file->parent.lock(); }
void setParent(std::shared_ptr<Directory> parent) { file->parent = parent; }
std::shared_ptr<File> unlocked() { return file; }
};
class DataFile::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;
[[nodiscard]] int open(oflags_t flags) { return getFile()->open(flags); }
[[nodiscard]] int close() { return getFile()->close(); }
ssize_t read(uint8_t* buf, size_t len, off_t offset) {
return getFile()->read(buf, len, offset);
}
ssize_t write(const uint8_t* buf, size_t len, off_t offset) {
return getFile()->write(buf, len, offset);
}
[[nodiscard]] int setSize(off_t size) { return getFile()->setSize(size); }
// TODO: Design a proper API for flushing files.
[[nodiscard]] int flush() { return getFile()->flush(); }
// 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);
};
class Directory::Handle : public File::Handle {
std::shared_ptr<Directory> getDir() { return file->cast<Directory>(); }
void cacheChild(const std::string& name,
std::shared_ptr<File> child,
DCacheKind kind);
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) {}
// Retrieve the child if it is in the dcache and otherwise forward the request
// to the backend, caching any `File` object it returns.
std::shared_ptr<File> getChild(const std::string& name);
// Add a child to this directory's entry cache without actually inserting it
// in the underlying backend. Assumes a child with this name does not already
// exist. Return `true` on success and `false` otherwise.
bool mountChild(const std::string& name, std::shared_ptr<File> file);
// Insert a child of the given name, kind, and mode in the underlying backend,
// which will allocate and return a corresponding `File` on success or return
// nullptr otherwise. Assumes a child with this name does not already exist.
// If the operation failed, returns nullptr.
std::shared_ptr<DataFile> insertDataFile(const std::string& name,
mode_t mode);
std::shared_ptr<Directory> insertDirectory(const std::string& name,
mode_t mode);
std::shared_ptr<Symlink> insertSymlink(const std::string& name,
const std::string& target);
// Move the file represented by `file` from its current directory to this
// directory with the new `name`, possibly overwriting another file that
// already exists with that name. The old directory may be the same as this
// directory. On success return 0 and otherwise return a negative error code
// without changing any underlying state. This should only be called from
// renameat with the locks on the old and new parents already held.
[[nodiscard]] int insertMove(const std::string& name,
std::shared_ptr<File> file);
// Remove the file with the given name. Returns zero on success or if the
// child has already been removed and otherwise returns a negative error code
// if the child cannot be removed.
[[nodiscard]] int removeChild(const std::string& name);
std::string getName(std::shared_ptr<File> file);
[[nodiscard]] ssize_t getNumEntries();
[[nodiscard]] MaybeEntries getEntries();
};
inline File::Handle File::locked() { return Handle(shared_from_this()); }
inline DataFile::Handle DataFile::locked() {
return Handle(shared_from_this());
}
inline Directory::Handle Directory::locked() {
return Handle(shared_from_this());
}
} // namespace wasmfs