475 lines
14 KiB
C++
Executable File
475 lines
14 KiB
C++
Executable File
module;
|
|
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
|
|
export module turtle;
|
|
|
|
import coral;
|
|
import coral.files;
|
|
import coral.io;
|
|
|
|
using coral::closure;
|
|
using coral::io_error;
|
|
using coral::slice;
|
|
using coral::unreachable;
|
|
using coral::usize;
|
|
|
|
export namespace turtle {
|
|
/**
|
|
* Path to a native I/O resource that is big enough for every supported platform.
|
|
*/
|
|
struct native_path {
|
|
/**
|
|
* Errors that may occur during a path joining operation.
|
|
*
|
|
* [join_error::overflow] signals that the given path join exceeds the maximum valid length of a native path.
|
|
*/
|
|
enum class join_error {
|
|
overflow,
|
|
};
|
|
|
|
/**
|
|
* Maximum number of bytes in a native path.
|
|
*/
|
|
static usize const max = 4095;
|
|
|
|
native_path() = default;
|
|
|
|
/**
|
|
* Constructs a native path from `text`, raising a static assertion if it is larger than [max].
|
|
*/
|
|
template<usize text_size> constexpr native_path(char const(&text)[text_size]) {
|
|
static_assert(text_size <= max);
|
|
|
|
for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i];
|
|
|
|
this->buffer[text_size] = 0;
|
|
}
|
|
|
|
/**
|
|
* Returns a weak reference to the native path as a [coral::slice].
|
|
*
|
|
* *Note*: this is an `O(N)` time function, where `N` is the path length.
|
|
*/
|
|
constexpr slice<char const> as_slice() const {
|
|
return {this->buffer, this->filled()};
|
|
}
|
|
|
|
/**
|
|
* Returns the number of bytes composing the native path.
|
|
*
|
|
* *Note*: this is an `O(N)` time function, where `N` is the path length.
|
|
*/
|
|
constexpr usize filled() const {
|
|
usize length {0};
|
|
|
|
while (this->buffer[length]) length += 1;
|
|
|
|
return length;
|
|
}
|
|
|
|
/**
|
|
* Attempts to create a new native path from the current native path joined with `text`, returning it or a
|
|
* [join_error].
|
|
*
|
|
* *Note*: this is an `O(N)` time function, where `N` is the path length.
|
|
*/
|
|
constexpr coral::expected<native_path, join_error> joined(slice<char const> const & text) const {
|
|
usize buffer_filled {this->filled()};
|
|
|
|
if (text.length >= (max - buffer_filled)) return join_error::overflow;
|
|
|
|
native_path joined_path {*this};
|
|
|
|
for (char const c : text) {
|
|
joined_path.buffer[buffer_filled] = c;
|
|
buffer_filled += 1;
|
|
}
|
|
|
|
return joined_path;
|
|
}
|
|
|
|
private:
|
|
char buffer[max + 1] {0};
|
|
};
|
|
|
|
/**
|
|
* Results from a native I/O resource closing operation.
|
|
*
|
|
* [close_result::ok] means that no errors occured and the resource has been successfully closed if it was
|
|
* open.
|
|
*
|
|
* [close_result::io_unavailable] is a generic error to communicate that something between the hardware and the
|
|
* operating system layer failed while closing the resource.
|
|
*
|
|
* [close_result::access_denied] reports that the process does not have the required permissions to open the
|
|
* resource. This is a rare but possible error that is only possible by a system changing the underlying
|
|
* resource permissions after the native resource has already opened it.
|
|
*/
|
|
enum class [[nodiscard]] close_result {
|
|
ok,
|
|
io_unavailable,
|
|
access_denied,
|
|
};
|
|
|
|
/**
|
|
* Results from a native I/O resource opening operation.
|
|
*
|
|
* [open_result::ok] means that no errors occured and the resource has been successfully opened.
|
|
*
|
|
* [open_result::io_unavailable] is a generic error to communicate that something between the hardware and the
|
|
* operating system layer failed while opening the resource.
|
|
*
|
|
* [open_result::access_denied] reports that the process does not have the required permissions to open the
|
|
* resource. While all platforms implement some form of file permissions, the specifics of this error are
|
|
* opaque to the caller.
|
|
*
|
|
* [open_result::not_found] indicates that no resource matching the opening query was found.
|
|
*
|
|
* [open_result::too_many] signals that there are too many files open in the current process and / or the
|
|
* wider operating system at the moment to open the resource.
|
|
*
|
|
* [open_result::too_big] signals that the resource is too big to open. The usual cause of this is error is
|
|
* attempting to open a file bigger than the addressable file range supported by the compiled application.
|
|
*
|
|
* [open_result::out_of_memory] signals that the system does not have enough memory remaining to open the resource.
|
|
*/
|
|
enum class [[nodiscard]] open_result {
|
|
ok,
|
|
io_unavailable,
|
|
access_denied,
|
|
not_found,
|
|
too_many,
|
|
too_big,
|
|
out_of_memory,
|
|
};
|
|
|
|
/**
|
|
* Maps a [close_result] to the equivalent [open_result] and returns the value.
|
|
*/
|
|
open_result close_to_open_result(close_result result) {
|
|
switch (result) {
|
|
case close_result::ok: return open_result::ok;
|
|
case close_result::io_unavailable: return open_result::io_unavailable;
|
|
case close_result::access_denied: return open_result::access_denied;
|
|
default: unreachable();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides unmanaged access to a native file.
|
|
*/
|
|
struct native_file final : public coral::file_reader, public coral::file_writer {
|
|
/**
|
|
* Opening modes for files.
|
|
*
|
|
* [open_mode::read_only] requests read-only access to an existing file. Specifying a path to an invalid file
|
|
* will result in the open request failing.
|
|
*
|
|
* [open_mode::overwrite] requests write-only access to a new file. Specifying a path to an existing file will
|
|
* result in it being wiped and overwritten (assuming it is not protected by the underlying operating system).
|
|
*
|
|
* [open_mode::append] requests write-only access to an existing file. Specifying a path to an invalid file
|
|
* will result in the open request failing. The write cursor begins at the end of any existing data in the file.
|
|
*/
|
|
enum class open_mode {
|
|
read_only,
|
|
overwrite,
|
|
append,
|
|
};
|
|
|
|
native_file() = default;
|
|
|
|
/**
|
|
* Attempts to close any currently open file.
|
|
*
|
|
* A [close_result] is returned containing either [close_result::ok] to indicate success or any other value to
|
|
* indicate an error. See [close_result] for more details.
|
|
*
|
|
* *Note*: failing to close should not be treated as a reason to retry the closing operation, and should instead
|
|
* be used to inform the end-user that the operation failed or that the process should exit.
|
|
*/
|
|
close_result close() {
|
|
errno = 0;
|
|
|
|
if (::close(this->fd) != 0) switch (errno) {
|
|
case EINTR: case EIO: case ENOSPC: return close_result::io_unavailable;
|
|
case EDQUOT: return close_result::access_denied;
|
|
default: unreachable();
|
|
}
|
|
|
|
this->fd = 0;
|
|
|
|
return close_result::ok;
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if a file is currently open, otherwise `false`.
|
|
*/
|
|
bool is_open() const {
|
|
return this->fd > 0;
|
|
}
|
|
|
|
/**
|
|
* Attempts to open a native file at `file_path` using `file_open_mode` for the access policy. See [open_mode]
|
|
* for more information on how files may be opened.
|
|
*
|
|
* An [open_result] is returned containing either [open_result::ok] to indicate success or any other value to
|
|
* indicate an error. See [open_result] for more details.
|
|
*
|
|
* *Note*: the opened file must be closed using [close] once no longer needed or the process will leak file
|
|
* handles.
|
|
*
|
|
* *Note*: it is recommended to prefer performing file I/O via [sandboxed_fs] unless direct file access is
|
|
* required.
|
|
*/
|
|
open_result open(native_path const & file_path, open_mode file_open_mode) {
|
|
if (this->is_open()) {
|
|
open_result const result {close_to_open_result(this->close())};
|
|
|
|
if (result != open_result::ok) return result;
|
|
}
|
|
|
|
constexpr int perms {S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH};
|
|
|
|
errno = 0;
|
|
|
|
switch (file_open_mode) {
|
|
case open_mode::read_only: {
|
|
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_RDONLY, perms);
|
|
|
|
break;
|
|
}
|
|
|
|
case open_mode::overwrite: {
|
|
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_CREAT | O_TRUNC, perms);
|
|
|
|
break;
|
|
}
|
|
|
|
case open_mode::append: {
|
|
this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_APPEND | O_CREAT, perms);
|
|
|
|
break;
|
|
}
|
|
|
|
default: unreachable();
|
|
}
|
|
|
|
if (!this->is_open()) switch (errno) {
|
|
case EACCES: case EDQUOT: case ENXIO: case EPERM:
|
|
case EROFS: case ETXTBSY: return open_result::access_denied;
|
|
|
|
case EINTR: case ENOSPC: return open_result::io_unavailable;
|
|
|
|
case EINVAL: case EISDIR: case ELOOP: case ENAMETOOLONG:
|
|
case ENOENT: case ENOTDIR: return open_result::not_found;
|
|
|
|
case EOVERFLOW: case EFBIG: return open_result::too_big;
|
|
case EMFILE: case ENFILE: return open_result::too_many;
|
|
case ENOMEM: return open_result::out_of_memory;
|
|
default: unreachable();
|
|
}
|
|
|
|
return open_result::ok;
|
|
}
|
|
|
|
/**
|
|
* See [coral::file_reader::read].
|
|
*/
|
|
coral::expected<usize, io_error> read(slice<coral::u8> const & data) override {
|
|
if (!this->is_open()) return io_error::unavailable;
|
|
|
|
coral::size const data_read {::read(this->fd, data.pointer, sizeof(coral::u8) * data.length)};
|
|
|
|
if (data_read < 0) return io_error::unavailable;
|
|
|
|
return data_read;
|
|
}
|
|
|
|
/**
|
|
* See [coral::file_reader::seek] and [coral::file_writer::seek].
|
|
*/
|
|
coral::expected<coral::u64, io_error> seek(coral::u64 offset) override {
|
|
if (!this->is_open()) return io_error::unavailable;
|
|
|
|
coral::i64 const data_position {::lseek(this->fd,
|
|
static_cast<coral::i64>(coral::min(offset, coral::i64_max)), SEEK_SET)};
|
|
|
|
if (data_position == -1) return io_error::unavailable;
|
|
|
|
return static_cast<coral::u64>(data_position);
|
|
}
|
|
|
|
/**
|
|
* See [coral::file_reader::tell] and [coral::file_writer::tell].
|
|
*/
|
|
coral::expected<coral::u64, io_error> tell() override {
|
|
if (!this->is_open()) return io_error::unavailable;
|
|
|
|
coral::i64 const data_position {::lseek(this->fd, 0, SEEK_SET)};
|
|
|
|
if (data_position == -1) return io_error::unavailable;
|
|
|
|
return static_cast<coral::u64>(data_position);
|
|
}
|
|
|
|
/**
|
|
* See [coral::file_writer::write].
|
|
*/
|
|
coral::expected<usize, io_error> write(slice<coral::u8 const> const & data) override {
|
|
if (!this->is_open()) return io_error::unavailable;
|
|
|
|
coral::size const data_written {::write(this->fd, data.pointer, sizeof(coral::u8) * data.length)};
|
|
|
|
if (data_written < 0) return io_error::unavailable;
|
|
|
|
return data_written;
|
|
}
|
|
|
|
private:
|
|
int fd {0};
|
|
};
|
|
|
|
/**
|
|
* [coral::file_system] wrapper around native file system access to provide a managed and system-agnostic
|
|
* environment for performing file I/O.
|
|
*/
|
|
struct file_sandbox : public coral::file_system {
|
|
/**
|
|
* Permission flags that a [sandboxed_fs] may specify for restricting access to it.
|
|
*/
|
|
struct permissions {
|
|
bool can_read;
|
|
|
|
bool can_write;
|
|
|
|
bool can_walk;
|
|
};
|
|
|
|
/**
|
|
* Constructs a sandbox located at `path` with `sandbox_permissions` as the permissions given to users of it.
|
|
*/
|
|
file_sandbox(native_path const & path, permissions const & access_permissions) {
|
|
this->path = path;
|
|
this->access_permissions = access_permissions;
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to a [file_sandbox] that provides access to the base directory which, on most systems, is
|
|
* the current working directory.
|
|
*
|
|
* The base directory may be used to access things loose files created outside of the application, such as user-
|
|
* generated files and modifications.
|
|
*
|
|
* *Note*: The base file system does not permit being written to.
|
|
*/
|
|
static file_sandbox & base() {
|
|
static file_sandbox base_sandbox {"./", {
|
|
.can_read = true,
|
|
.can_walk = true
|
|
}};
|
|
|
|
return base_sandbox;
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to a [file_sandbox] that operates as a temporary file store.
|
|
*
|
|
* As the name implies, the existence of files that exist here are not guaranteed beyond the duration of an
|
|
* application run lifetime. The purpose of the temporary file store is to support transactional I/O
|
|
* operations that only want to replace files if new ones has been successfully constructed already.
|
|
*
|
|
* *Note*: The temp file system does not permit being walked.
|
|
*/
|
|
static file_sandbox & temp() {
|
|
static file_sandbox temp_sandbox {"/tmp", {
|
|
.can_read = true,
|
|
.can_write = false,
|
|
}};
|
|
|
|
return temp_sandbox;
|
|
}
|
|
|
|
/**
|
|
* See [coral::fs::read_file].
|
|
*
|
|
* *Note*: this function will only work on sandboxes with the [permissions::can_read] flag enabled.
|
|
*/
|
|
void read_file(slice<char const> const & path, closure<void(coral::file_reader &)> const & then) override {
|
|
if (!this->access_permissions.can_read) return;
|
|
|
|
this->path.joined(path).and_then([&](native_path const & native_file_path) -> void {
|
|
native_file file;
|
|
|
|
if (file.open(native_file_path, native_file::open_mode::read_only) != open_result::ok) return;
|
|
|
|
then(file);
|
|
|
|
if (file.close() != close_result::ok) return;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* See [coral::fs::walk_files].
|
|
*
|
|
* *Note*: this function will only work on sandboxes with the [permissions::can_walk] flag enabled.
|
|
*/
|
|
void walk_files(slice<char const> const & path, closure<void(coral::file_walker &)> const & then) override {
|
|
if (!this->access_permissions.can_walk) return;
|
|
|
|
this->path.joined(path).and_then([&](native_path const & native_directory_path) -> void {
|
|
// TODO: Implement.
|
|
});
|
|
}
|
|
|
|
/**
|
|
* See [coral::fs::write_file].
|
|
*
|
|
* *Note*: this function will only work on sandboxes with the [permissions::can_write] flag enabled.
|
|
*/
|
|
void write_file(slice<char const> const & path, closure<void(coral::file_writer &)> const & then) override {
|
|
if (!this->access_permissions.can_write) return;
|
|
|
|
this->path.joined(path).and_then([&](native_path const & native_file_path) -> void {
|
|
native_file file;
|
|
|
|
if (file.open(native_file_path, native_file::open_mode::overwrite) != open_result::ok) return;
|
|
|
|
then(file);
|
|
|
|
if (file.close() != close_result::ok) return;
|
|
});
|
|
}
|
|
|
|
private:
|
|
native_path path;
|
|
|
|
permissions access_permissions {
|
|
.can_read = false,
|
|
.can_write = false,
|
|
.can_walk = false,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Returns a reference to the process-wide error logging [coral::writer] used for recording errors.
|
|
*/
|
|
coral::writer & error_log() {
|
|
static struct : public coral::writer {
|
|
coral::expected<usize, io_error> write(slice<coral::u8 const> const & data) override {
|
|
coral::size const data_written {::write(STDOUT_FILENO, data.pointer, sizeof(coral::u8) * data.length)};
|
|
|
|
if (data_written == -1) return io_error::unavailable;
|
|
|
|
return data_written;
|
|
};
|
|
} output_writer;
|
|
|
|
return output_writer;
|
|
}
|
|
}
|