319 lines
10 KiB
Zig
319 lines
10 KiB
Zig
|
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 = "/",
|
||
|
});
|
||
|
}
|
||
|
};
|