module; #include #include #include #include export module turtle; import coral; import coral.files; import coral.io; using coral::closure; using coral::io_error; using coral::path; 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 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 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 joined(slice 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 read(slice 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 seek(coral::u64 offset) override { if (!this->is_open()) return io_error::unavailable; coral::i64 const data_position {::lseek(this->fd, static_cast(coral::min(offset, coral::i64_max)), SEEK_SET)}; if (data_position == -1) return io_error::unavailable; return static_cast(data_position); } /** * See [coral::file_reader::tell] and [coral::file_writer::tell]. */ coral::expected 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(data_position); } /** * See [coral::file_writer::write]. */ coral::expected write(slice 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}; }; /** * Provides unmanaged access to a native directory. */ struct native_directory final : public coral::file_walker { native_directory() = default; /** * Attempts to close any currently open directory. * * 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() { if (closedir(this->dir) == 0) return close_result::io_unavailable; return close_result::ok; } /** * See [coral::file_walker::has_next]. */ bool has_next() override { return this->entry != nullptr; } /** * Returns `true` if a directory is currently open, otherwise `false`. */ bool is_open() const { return this->dir != nullptr; } /** * See [coral::file_walker::next]. */ coral::expected next() override { usize name_length {0}; constexpr usize name_max {sizeof(dirent::d_name) / sizeof(char)}; while ((name_length < name_max) && (this->entry->d_name[name_length] != 0)) name_length += 1; path current_path {path{}.joined(slice{this->entry->d_name, name_length})}; errno = 0; this->entry = readdir(this->dir); if (this->entry == nullptr) switch (errno) { case EBADF: return io_error::unavailable; default: unreachable(); } return current_path; } /** * Attempts to open a native directory at `directory_path`. * * 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 directory must be closed using [close] once no longer needed or the process will leak * directory streams. * * *Note*: if a directory is currently open under the native directory, it will attempt to close it before * proceeding with opening the next. This means that [open] is safe to call without first calling [close]. * * *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 & directory_path) { if (this->is_open()) { open_result const result {close_to_open_result(this->close())}; if (result != open_result::ok) return result; } // No room for zero terminator. errno = 0; this->dir = opendir(directory_path.as_slice().as_chars().pointer); if (!this->is_open()) switch (errno) { case EACCES: return open_result::access_denied; case EMFILE: case ENFILE: return open_result::too_many; case ENOENT: case ENOTDIR: return open_result::not_found; case ENOMEM: return open_result::out_of_memory; } errno = 0; this->entry = readdir(this->dir); if (this->entry == nullptr) switch (errno) { case EBADF: { if (this->is_open()) { open_result const result {close_to_open_result(this->close())}; if (result != open_result::ok) return result; } return open_result::io_unavailable; } default: unreachable(); } return open_result::ok; } private: DIR * dir {nullptr}; dirent * entry {nullptr}; }; /** * [coral::fs] wrapper around native file system access to provide a managed and system-agnostic environment for * performing file I/O. */ struct sandboxed_fs : public coral::fs { /** * 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 `sandbox_path` with `sandbox_permissions` as the permissions given to users * of it. */ sandboxed_fs(native_path const & sandbox_path, permissions const & access_permissions) { this->path = sandbox_path; this->access_permissions = access_permissions; } /** * Returns a reference to a [sandboxed_fs] 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 sandboxed_fs & base() { static sandboxed_fs base_fs {"./", { .can_read = true, .can_walk = true }}; return base_fs; } /** * Returns a reference to a [sandboxed_fs] 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 sandboxed_fs & temp() { static sandboxed_fs base_fs {"/tmp", { .can_read = true, .can_write = false, }}; return base_fs; } /** * See [coral::fs::read_file]. * * *Note*: this function will only work on sandboxes with the [permissions::can_read] flag enabled. */ void read_file(path const & target_path, closure const & then) override { if (!this->access_permissions.can_read) return; this->path.joined(target_path.as_slice()).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); // TODO: Error orphaned file handle! 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(path const & target_path, closure const & then) override { if (!this->access_permissions.can_walk) return; this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_directory_path) -> void { native_directory directory; if (directory.open(native_directory_path) == open_result::ok) return; then(directory); // TODO: Error orphaned file handle! if (directory.close() != close_result::ok) return; }); } /** * See [coral::fs::write_file]. * * *Note*: this function will only work on sandboxes with the [permissions::can_write] flag enabled. */ void write_file(path const & target_path, closure const & then) override { if (!this->access_permissions.can_write) return; this->path.joined(target_path.as_slice()).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); // TODO: Error orphaned file handle! 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 output device used for writing data out from to the wider system. * * This [coral::writer] is particularly useful for command-line tools which require communicating with another * process via pipes or an end-user via the shell. */ coral::writer & output() { static struct : public coral::writer { coral::expected write(slice 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; } }