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::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}; }; /** * [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 const & path, closure const & then) override { if (!this->access_permissions.can_read) return; this->path.joined(path).ok().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 const & path, closure const & then) override { if (!this->access_permissions.can_walk) return; this->path.joined(path).ok().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 const & path, closure const & then) override { if (!this->access_permissions.can_write) return; this->path.joined(path).ok().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 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; } }