ona/src/sys.zig

319 lines
10 KiB
Zig
Raw Normal View History

2022-09-25 00:09:02 +02:00
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const mem = @import("./mem.zig");
const stack = @import("./stack.zig");
const std = @import("std");
///
///
///
pub const EventLoop = packed struct {
current_request: ?*Request = null,
///
/// With files typically being backed by a block device, they can produce a variety of errors -
/// from physical to virtual errors - these are all encapsulated by the API as general
/// [Error.Inaccessible] errors.
///
pub const FileError = error {
Inaccessible,
};
///
/// [OpenError.NotFound] is used as a catch-all for any hardware or software-specific reason for
/// failing to open a given file. This includes file-system restrictions surrounding a specific
/// file as well as it simply not existing.
///
/// [OpenError.OutOfFiles] occurs when there are no more resources available to open further
/// files. As a result, some open files must be closed before more may be opened.
///
pub const OpenError = error {
NotFound,
OutOfFiles,
};
///
/// Indicates what kind of access the consumer logic has to a file.
///
/// [OpenMode.read] is for reading from an existing file from the start.
///
/// [OpenMode.overwrite] is for deleting the contents of a file, or creating an empty one if no
/// such file exists, and writing to it from the start.
///
/// [OpenMode.append] is for writing additional contents to a file, creating an empty one if no
/// such file exists, on the end of whatever it already contains.
///
pub const OpenMode = enum {
read,
overwrite,
append,
};
const Request = struct {
next: ?*Request = null,
frame: anyframe,
message: union(enum) {
close: struct {
file: *ext.SDL_RWops,
},
open: struct {
path: *const Path,
file: OpenError!*ext.SDL_RWops,
mode: OpenMode,
},
},
};
const max_files = 512;
///
/// Asynchronously closes `file_access` via `event_loop`.
///
/// *Note* that `file_access` must have been opened by `event_loop` for it to be closed by it,
/// otherwise it will cause undefined behavior.
///
pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void {
var request = Request{
.frame = @frame(),
.message = .{.close = @ptrCast(*ext.SDL_RWops, file_access)},
};
suspend {
if (event_loop.current_request) |current_request| {
current_request.next = &request;
} else {
event_loop.current_request = &request;
}
}
}
///
/// Asynchronously attempts to open access to a file at `path` via `event_loop`, with `mode` as
/// the preferences for how it should be opened.
///
/// A reference to a [FileAccess] representing the bound file is returned if the operation was
/// successful, otherwise an [OpenError] if the file could not be opened.
///
/// *Note* that, regardless of platform, files will always be treated as containing binary data.
///
pub fn open(event_loop: *EventLoop, path: Path, mode: OpenMode) OpenError!*FileAccess {
var request = Request{
.frame = @frame(),
.message = .{
.open = .{
.path = &path,
.file = error.OutOfFiles,
.mode = mode,
},
},
};
suspend {
if (event_loop.current_request) |current_request| {
current_request.next = &request;
} else {
event_loop.current_request = &request;
}
}
return @ptrCast(*FileAccess, try request.message.open.file);
}
///
///
///
pub fn tick(event_loop: *EventLoop) void {
while (event_loop.current_request) |request| {
switch (request.message) {
.close => |*close| {
// Swallow file close errors.
_ = ext.SDL_RWclose(close.file);
},
.open => |*open| {
open.file = ext.SDL_RWFromFile(&open.path.buffer, switch (open.mode) {
.read => "rb",
.overwrite => "wb",
.append => "ab",
}) orelse error.NotFound;
},
}
resume request.frame;
event_loop.current_request = request.next;
}
}
};
pub const FileAccess = opaque {
};
///
/// Platform-agnostic mechanism for accessing files on any of the virtual file-systems supported by
/// Ona.
///
pub const Path = struct {
locator: Locator,
length: u16,
buffer: [max]u8,
///
/// Virtual file-system locators supported by Ona.
///
pub const Locator = enum(u8) {
relative,
data,
user,
};
///
/// Errors that may occur during parsing of an incorrectly formatted [Path] URI.
///
pub const ParseError = (JoinError || error {
BadLocator,
});
///
///
///
pub const JoinError = error {
TooLong,
};
///
/// Returns `true` if the length of `path` is empty, otherwise `false`.
///
pub fn isEmpty(path: Path) bool {
return (path.length == 0);
}
///
/// Attempts to lazily join `components` into a new [Path] value derived from `path`, returning
/// it when `components` has no more data or a [JoinError] if the operation failed.
///
/// Any duplicate path components, such as trailing ASCII forward-slashes (`/`) or periods
/// (`.`), will be normalized to a more concise internal representation.
///
/// *Note* that `components` may be mutated during execution of the operation.
///
pub fn joinSpliterator(path: Path, components: *mem.Spliterator) JoinError!Path {
var joined_path = path;
var path_buffer = stack.Unmanaged{
.buffer = &joined_path.buffer,
.filled = if (joined_path.length == 0) joined_path.length else (joined_path.length - 1),
};
if (components.next()) |component| switch (component.len) {
0 => if (joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong,
1 => if ((component[0] == '.') and joined_path.isEmpty())
path_buffer.push("./") catch return error.TooLong,
else => {
if (!joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong;
path_buffer.pushAll(component) catch return error.TooLong;
if (components.hasNext()) path_buffer.push('/') catch return error.TooLong;
},
};
while (components.next()) |component|
if ((component.len != 0) or (!((component.len == 1) and (component[0] == '.')))) {
if (!joined_path.isEmpty()) path_buffer.push('/') catch return error.TooLong;
path_buffer.pushAll(component) catch return error.TooLong;
if (components.hasNext()) path_buffer.push('/') catch return error.TooLong;
};
// No space left over for the null terminator.
if (path_buffer.filled >= max) return error.TooLong;
joined_path.length = path_buffer.filled;
return joined_path;
}
///
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
///
pub fn equals(this: Path, that: Path) bool {
return (this.locator == that.locator) and
std.mem.eql(this.buffer[0 .. this.length], that.buffer[0 .. that.length]);
}
///
/// Creates and returns an empty [Path] value rooted at the location of `locator`.
///
pub fn from(locator: Locator) Path {
return .{
.locator = locator,
.length = 0,
.buffer = std.mem.zeroes([max]u8),
};
}
///
/// The maximum possible byte-length of a [Path].
///
/// Note that paths are encoded using UTF-8, meaning that a character may be bigger than one
/// byte. Because of this, it is not safe to asume that a path may hold [max] individual
/// characters.
///
pub const max = 1000;
///
/// Attempts to parse the data in `sequence`, returning the [Path] value or an error from
/// [ParseError] if it failed to parse.
///
/// The rules surrounding the encoding of `sequence` are loose, with the only fundamental
/// requirements being:
///
/// * It starts with a locator key followed by an ASCII colon (`data:`, `user:`, etc.)
/// followed by the rest of the path.
///
/// * Each component of the path is separated by an ASCII forward-slash (`/`).
///
/// * A path that begins with an ASCII forward-slash ('/') after the locator key is considered
/// to be relative to the root of the specified locator key instead of relative to the path
/// caller.
///
/// Additional encoding rules are enforced via underlying file-system being used. For example,
/// Microsoft Windows is case-insensitive while Unix and Linux systems are not. Additionally,
/// Windows has far more reserved characters and sequences which cannot be used when interfacing
/// with files compared to Linux and Unix systems.
///
/// See [ParseError] for more information on the kinds of errors that may be returned.
///
pub fn parse(sequence: []const u8) ParseError!Path {
if (sequence.len == 0) return Path.from(.relative);
if (mem.forwardFind(u8, sequence, ':')) |locator_path_delimiter_index| {
var locator = std.meta.stringToEnum(Locator,
sequence[0 .. locator_path_delimiter_index]) orelse return error.BadLocator;
const components_index = (locator_path_delimiter_index + 1);
return Path.from(locator).joinSpliterator(&.{
.source = sequence[components_index .. (sequence.len - components_index)],
.delimiter = "/",
});
}
return Path.from(.relative).joinSpliterator(&.{
.source = sequence,
.delimiter = "/",
});
}
};