Split event loop into its own file
This commit is contained in:
parent
6fb6e63280
commit
94b1d8f67e
|
@ -1,7 +0,0 @@
|
|||
|
||||
///
|
||||
/// Returns `true` if `value` did not return `Error`, otherwise `false`.
|
||||
///
|
||||
pub fn isOk(comptime Error: type, value: Error!void) bool {
|
||||
return if (value) |_| true else |_| false;
|
||||
}
|
470
src/io.zig
470
src/io.zig
|
@ -1,476 +1,6 @@
|
|||
const stack = @import("./stack.zig");
|
||||
const std = @import("std");
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const Path = struct {
|
||||
length: u16,
|
||||
buffer: [max]u8,
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const empty = std.mem.zeroes(Path);
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub fn equalsText(path: Path, text: []const u8) bool {
|
||||
return std.mem.eql(u8, path.buffer[0 .. path.length], text);
|
||||
}
|
||||
|
||||
///
|
||||
/// 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 `2048` individual
|
||||
/// characters.
|
||||
///
|
||||
pub const max = 2048;
|
||||
};
|
||||
|
||||
///
|
||||
/// Universal resource identifier (URI) that operates atop the operating system to provide a
|
||||
/// platform-agnostic interface for local and networked I/O access.
|
||||
///
|
||||
/// For more information, see [https://en.wikipedia.org/wiki/URI].
|
||||
///
|
||||
pub const Uri = struct {
|
||||
buffer: [max]u8,
|
||||
scheme_len: u16,
|
||||
user_info_range: Range,
|
||||
host_range: Range,
|
||||
port_number: u16,
|
||||
path_range: Range,
|
||||
|
||||
///
|
||||
/// Errors that may occur during parsing of a URI from URI-compatible source encoding.
|
||||
///
|
||||
/// [ParseError.TooLong] occurs when the provided source data is bigger than the max allowed
|
||||
/// data representation in [max].
|
||||
///
|
||||
/// [ParseError.UnexpectedToken] occurs when the internal tokenization process encounters a
|
||||
/// URI component token in the wrong order.
|
||||
///
|
||||
/// [ParseError.InvalidEncoding] occurs when the source encoding being parsed is not properly
|
||||
/// encoded in its own format (malformed UTF-8, for example).
|
||||
///
|
||||
pub const ParseError = error {
|
||||
TooLong,
|
||||
UnexpectedToken,
|
||||
InvalidEncoding,
|
||||
};
|
||||
|
||||
const Range = struct {
|
||||
off: u16,
|
||||
len: u16,
|
||||
|
||||
const none = std.mem.zeroes(Range);
|
||||
};
|
||||
|
||||
///
|
||||
/// Represents an individual component of a URI sequence.
|
||||
///
|
||||
pub const Token = union(enum) {
|
||||
scheme: []const u8,
|
||||
user_info: []const u8,
|
||||
host: []const u8,
|
||||
port: []const u8,
|
||||
path: []const u8,
|
||||
query: []const u8,
|
||||
fragment: []const u8,
|
||||
};
|
||||
|
||||
///
|
||||
/// Tokenizes the data in [Tokenizer.utf8_sequence] into URI tokens.
|
||||
///
|
||||
/// See [Component] for more information on the supported URI tokens.
|
||||
///
|
||||
pub const Tokenizer = struct {
|
||||
cursor: usize = 0,
|
||||
utf8_sequence: []const u8,
|
||||
|
||||
///
|
||||
/// Extracts the next [Token] in sequence from `tokenizer` and returns it or `null` if
|
||||
/// there are no more tokens to be extracted.
|
||||
///
|
||||
pub fn next(tokenizer: *Tokenizer) ?Token {
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'A' ... 'Z', 'a' ... 'z' => {
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
var is_scheme = (begin == 0);
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
'+', '.', '-', '0' ... '9', 'A' ... 'Z', 'a' ... 'z' =>
|
||||
tokenizer.cursor += 1,
|
||||
|
||||
':' => {
|
||||
if (is_scheme) {
|
||||
defer tokenizer.cursor += 1;
|
||||
|
||||
return Token{.scheme =
|
||||
tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
}
|
||||
|
||||
tokenizer.cursor += 1;
|
||||
},
|
||||
|
||||
'#', '?' => break,
|
||||
|
||||
else => {
|
||||
tokenizer.cursor += 1;
|
||||
is_scheme = false;
|
||||
},
|
||||
};
|
||||
|
||||
return Token{.path =
|
||||
tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
|
||||
'@' => {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'/', ':' => break,
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{.host =
|
||||
tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
|
||||
':' => {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'/' => break,
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{
|
||||
.port = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
|
||||
'/' => {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
if (tokenizer.utf8_sequence[tokenizer.cursor] == '/') {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'@' => return Token{.user_info =
|
||||
tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]},
|
||||
|
||||
':', '/' => break,
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{
|
||||
.host = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
} else {
|
||||
const begin = (tokenizer.cursor - 1);
|
||||
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'?', '#' => break,
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{
|
||||
.path = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
}
|
||||
},
|
||||
|
||||
'?' => {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'#' => {
|
||||
tokenizer.cursor -= 1;
|
||||
|
||||
break;
|
||||
},
|
||||
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{
|
||||
.query = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
|
||||
'#' => {
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len) tokenizer.cursor += 1;
|
||||
|
||||
return Token{
|
||||
.fragment = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
|
||||
else => {
|
||||
const begin = tokenizer.cursor;
|
||||
|
||||
tokenizer.cursor += 1;
|
||||
|
||||
while (tokenizer.cursor < tokenizer.utf8_sequence.len)
|
||||
switch (tokenizer.utf8_sequence[tokenizer.cursor]) {
|
||||
|
||||
'#', '?' => break,
|
||||
else => tokenizer.cursor += 1,
|
||||
};
|
||||
|
||||
return Token{
|
||||
.path = tokenizer.utf8_sequence[begin .. (tokenizer.cursor - begin)]};
|
||||
},
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
/// A more constrained variant of [next] that accepts a `expected_token` argument to
|
||||
/// validate the component type of a [Token] before it is returned.
|
||||
///
|
||||
/// If the [Component] of the extracted [Token] is not identical to `expected_token`,
|
||||
/// it will be discarded and `null` is returned instead.
|
||||
///
|
||||
pub fn nextExpect(tokenizer: *Tokenizer, expected_token: std.meta.Tag(Token)) ?Token {
|
||||
if (tokenizer.next()) |token| {
|
||||
if (token == expected_token) return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
pub const empty = std.mem.zeroes(Uri);
|
||||
|
||||
///
|
||||
/// The maximum possible byte-length of a [URI].
|
||||
///
|
||||
/// Note that a URI character may be encoded using multiple bytes, meaning that `2048` is not
|
||||
/// identical in meaning to `2048` URI characters.
|
||||
///
|
||||
pub const max = 2048;
|
||||
|
||||
///
|
||||
/// Returns `true` if `uri_scheme` matches the scheme contained in `uri`, otherwise `false`.
|
||||
///
|
||||
pub fn isScheme(uri: Uri, uri_scheme: []const u8) bool {
|
||||
return std.mem.eql(u8, uri.buffer[0 .. uri.scheme_len], uri_scheme);
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to parse and return a [URI] value out of `utf8_sequence`, otherwise returning
|
||||
/// [ParseError] if `utf8_sequence` is invalid in any way.
|
||||
///
|
||||
/// [ParseError.InvalidEncoding] occurs if the data encoded in `utf8_sequence` cannot be
|
||||
/// validated as UTF-8 or it contains an invalid ASCII decimal number encoding for its URL port.
|
||||
///
|
||||
/// See [ParseError] for more details on the other errors that may be returned.
|
||||
///
|
||||
pub fn parse(utf8_sequence: []const u8) ParseError!Uri {
|
||||
if (!std.unicode.utf8ValidateSlice(utf8_sequence)) return error.InvalidEncoding;
|
||||
|
||||
var uri = Uri.empty;
|
||||
|
||||
if (utf8_sequence.len != 0) {
|
||||
if (utf8_sequence.len > max) return error.TooLong;
|
||||
|
||||
var tokenizer = Tokenizer{.utf8_sequence = utf8_sequence};
|
||||
const scheme_token = tokenizer.nextExpect(.scheme) orelse return error.UnexpectedToken;
|
||||
var uri_buffer = stack.Unmanaged(u8){.buffer = &uri.buffer};
|
||||
const uri_writer = uri_buffer.asWriter();
|
||||
const assert = std.debug.assert;
|
||||
|
||||
// These write operations will never fail because the uri_buffer will be known to be big
|
||||
// enough by this point.
|
||||
assert(uri_writer.write(scheme_token.scheme) == scheme_token.scheme.len);
|
||||
assert(uri_writer.writeByte(':'));
|
||||
|
||||
// Downcast is safe because utf8_sequence can't be greater than u16 max.
|
||||
uri.scheme_len = @intCast(u16, scheme_token.scheme.len);
|
||||
|
||||
var last_token = scheme_token;
|
||||
|
||||
while (tokenizer.next()) |scheme_specific_token| {
|
||||
switch (scheme_specific_token) {
|
||||
.scheme => return error.UnexpectedToken,
|
||||
|
||||
.user_info => |user_info| {
|
||||
if (last_token != .scheme) return error.UnexpectedToken;
|
||||
|
||||
const delimiter = "//";
|
||||
|
||||
assert(uri_writer.write(delimiter) == delimiter.len);
|
||||
|
||||
uri.user_info_range = .{
|
||||
.off = @intCast(u16, uri_buffer.filled),
|
||||
.len = @intCast(u16, user_info.len),
|
||||
};
|
||||
|
||||
assert(uri_writer.write(user_info) == user_info.len);
|
||||
assert(uri_writer.writeByte('@'));
|
||||
},
|
||||
|
||||
.host => |host| {
|
||||
switch (last_token) {
|
||||
.scheme => {
|
||||
const delimiter = "//";
|
||||
|
||||
assert(uri_writer.write(delimiter) == delimiter.len);
|
||||
},
|
||||
|
||||
.user_info => {},
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
|
||||
assert(uri_writer.write(host) == host.len);
|
||||
},
|
||||
|
||||
.port => |port| {
|
||||
if (last_token != .host) return error.UnexpectedToken;
|
||||
|
||||
const port_radix = 10;
|
||||
|
||||
uri.port_number = std.fmt.parseInt(u16, port, port_radix) catch
|
||||
return error.InvalidEncoding;
|
||||
|
||||
assert(uri_writer.writeByte(':'));
|
||||
assert(uri_writer.write(port) == port.len);
|
||||
},
|
||||
|
||||
.path => |path| {
|
||||
if ((last_token != .scheme) and (last_token != .host) and
|
||||
(last_token != .port)) return error.UnexpectedToken;
|
||||
|
||||
uri.path_range = .{
|
||||
.off = @intCast(u16, uri_buffer.filled),
|
||||
.len = @intCast(u16, path.len),
|
||||
};
|
||||
|
||||
assert(uri_writer.write(path) == path.len);
|
||||
},
|
||||
|
||||
.query => |query| {
|
||||
if ((last_token != .scheme) and (last_token != .host) and
|
||||
(last_token != .port) and (last_token != .path))
|
||||
return error.UnexpectedToken;
|
||||
|
||||
assert(uri_writer.writeByte('?'));
|
||||
|
||||
uri.path_range = .{
|
||||
.off = @intCast(u16, uri_buffer.filled),
|
||||
.len = @intCast(u16, query.len),
|
||||
};
|
||||
|
||||
assert(uri_writer.write(query) == query.len);
|
||||
},
|
||||
|
||||
.fragment => |fragment| {
|
||||
if ((last_token != .scheme) or (last_token != .host) or
|
||||
(last_token != .port) or (last_token != .path) or
|
||||
(last_token != .query)) return error.UnexpectedToken;
|
||||
|
||||
assert(uri_writer.writeByte('#'));
|
||||
|
||||
uri.path_range = .{
|
||||
.off = @intCast(u16, uri_buffer.filled),
|
||||
.len = @intCast(u16, fragment.len),
|
||||
};
|
||||
|
||||
assert(uri_writer.write(fragment) == fragment.len);
|
||||
},
|
||||
}
|
||||
|
||||
last_token = scheme_specific_token;
|
||||
}
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
///
|
||||
/// Creates and returns a [Path] value from the path component of `uri`.
|
||||
///
|
||||
pub fn toPath(uri: Uri) Path {
|
||||
var path = Path{
|
||||
.length = uri.path_range.len,
|
||||
.buffer = std.mem.zeroes([Path.max]u8),
|
||||
};
|
||||
|
||||
std.mem.copy(u8, path.buffer[0 ..], uri.buffer[uri.path_range.off .. uri.path_range.len]);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
///
|
||||
/// Writes the path component of `uri` to `path_writer`, returning `true` if all bytes used to
|
||||
/// encode the path were successfully written, otherwise `false` if it was partially completed
|
||||
/// or not at all.
|
||||
///
|
||||
pub fn writePath(uri: Uri, path_writer: Writer) bool {
|
||||
return (path_writer.write(uri.buffer[uri.path_range.off ..
|
||||
uri.path_range.len]) == uri.path_range.len);
|
||||
}
|
||||
};
|
||||
|
||||
test "uri" {
|
||||
const testing = @import("std").testing;
|
||||
const empty_uri = Uri.empty;
|
||||
|
||||
try testing.expect(empty_uri.isScheme(""));
|
||||
try testing.expect(empty_uri.toPath().equalsText(""));
|
||||
|
||||
const scheme_only_uri = try Uri.parse("uri:");
|
||||
|
||||
try testing.expect(scheme_only_uri.isScheme("uri"));
|
||||
try testing.expect(scheme_only_uri.toPath().equalsText(""));
|
||||
|
||||
const absolute_file_path = "/path/to/file";
|
||||
const absolute_file_uri = try Uri.parse("file:" ++ absolute_file_path);
|
||||
|
||||
try testing.expect(absolute_file_uri.isScheme("file"));
|
||||
try testing.expect(absolute_file_uri.toPath().equalsText(absolute_file_path));
|
||||
|
||||
const relative_file_path = "path/to/file";
|
||||
const relative_file_uri = try Uri.parse("file:" ++ relative_file_path);
|
||||
|
||||
try testing.expect(relative_file_uri.isScheme("file"));
|
||||
try testing.expect(relative_file_uri.toPath().equalsText(relative_file_path));
|
||||
}
|
||||
|
||||
///
|
||||
/// Opaque interface to a "writable" resource, such as a block device, memory buffer, or network
|
||||
/// socket.
|
||||
|
|
59
src/main.zig
59
src/main.zig
|
@ -2,27 +2,10 @@ const ext = @cImport({
|
|||
@cInclude("SDL2/SDL.h");
|
||||
});
|
||||
|
||||
const errors = @import("./errors.zig");
|
||||
const io = @import("./io.zig");
|
||||
const stack = @import("./stack.zig");
|
||||
const std = @import("std");
|
||||
|
||||
const Request = struct {
|
||||
next: ?*Request = null,
|
||||
frame: anyframe,
|
||||
|
||||
message: union(enum) {
|
||||
close: struct {
|
||||
file: *ext.SDL_RWops,
|
||||
is_closed: *bool,
|
||||
},
|
||||
|
||||
open_readable: struct {
|
||||
uri: *const io.Uri,
|
||||
file: ?*ext.SDL_RWops,
|
||||
},
|
||||
},
|
||||
};
|
||||
const sys = @import("./sys.zig");
|
||||
|
||||
///
|
||||
/// Entry point.
|
||||
|
@ -73,7 +56,7 @@ pub fn main() anyerror!void {
|
|||
|
||||
defer ext.SDL_DestroyRenderer(renderer);
|
||||
|
||||
var request_chain = @as(?*Request, null);
|
||||
var event_loop = sys.EventLoop{};
|
||||
var is_running = true;
|
||||
|
||||
while (is_running) {
|
||||
|
@ -97,43 +80,7 @@ pub fn main() anyerror!void {
|
|||
}
|
||||
|
||||
ext.SDL_RenderPresent(renderer);
|
||||
|
||||
while (request_chain) |request_head| {
|
||||
const request = request_head;
|
||||
|
||||
request_chain = request_head.next;
|
||||
|
||||
switch (request.message) {
|
||||
.close => |*close| close.is_closed.* = (ext.SDL_RWclose(close.file) == 0),
|
||||
|
||||
.open_readable => |*open_readable| {
|
||||
if (open_readable.uri.isScheme("data")) {
|
||||
var path = stack.Fixed(u8, 4096).init();
|
||||
|
||||
// These can never fail as the sum of the potential bytes written will
|
||||
// always be less than 4096.
|
||||
path.pushAll("./") catch unreachable;
|
||||
std.debug.assert(open_readable.uri.writePath(path.asWriter()));
|
||||
|
||||
open_readable.file = ext.SDL_RWFromFile(&path.buffer, "r");
|
||||
} else if (open_readable.uri.isScheme("user")) {
|
||||
var path = stack.Fixed(u8, 4096).init();
|
||||
const isOk = errors.isOk;
|
||||
|
||||
// Cannot guarantee that the sum of potential bytes written will always be
|
||||
// less than path max.
|
||||
if (isOk(stack.FinitePushError, path.pushAll(pref_path)) and
|
||||
open_readable.uri.writePath(path.asWriter())) {
|
||||
|
||||
open_readable.file = ext.SDL_RWFromFile(&path.buffer, "r");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resume request.frame;
|
||||
}
|
||||
|
||||
event_loop.tick();
|
||||
ext.SDL_Delay(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
const std = @import("std");
|
||||
|
||||
///
|
||||
/// State machine for lazily computing all components of [Spliterator.source] that match the pattern
|
||||
/// in [Spliterator.delimiter].
|
||||
///
|
||||
pub fn Spliterator(comptime Element: type) type {
|
||||
return struct {
|
||||
source: []const Element,
|
||||
delimiter: []const Element,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
///
|
||||
/// Returns `true` if there is more data to be processed, otherwise `false`.
|
||||
///
|
||||
/// Note that [Spliterator.next] implicitly calls this function to determine if it should
|
||||
/// return another slice or `null`.
|
||||
///
|
||||
pub fn hasNext(self: Self) bool {
|
||||
return (self.source.len != 0);
|
||||
}
|
||||
|
||||
///
|
||||
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
|
||||
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
|
||||
///
|
||||
pub fn next(self: *Self) ?[]const Element {
|
||||
if (self.delimiter.len == 0) {
|
||||
defer self.source = self.source[self.source.len .. 0];
|
||||
|
||||
return self.source;
|
||||
}
|
||||
|
||||
while (self.hasNext()) {
|
||||
var cursor = @as(usize, 0);
|
||||
var window = self.source[cursor .. (self.source - cursor)];
|
||||
|
||||
defer self.source = window;
|
||||
|
||||
if (std.mem.eql(Element, window, self.delimiter))
|
||||
return self.source[cursor .. self.delimiter.len];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Searches the slice of `Data` referenced by `data` for the first instance of `sought_datum`,
|
||||
/// returning its index or `null` if it could not be found.
|
||||
///
|
||||
pub fn findFirst(comptime Data: type, data: []const Data, sought_datum: Data) ?usize {
|
||||
for (data) |datum, index| if (datum == sought_datum) return index;
|
||||
|
||||
return null;
|
||||
}
|
|
@ -57,16 +57,6 @@ pub fn Fixed(comptime Element: type, comptime capacity: usize) type {
|
|||
return self.filled;
|
||||
}
|
||||
|
||||
///
|
||||
/// Creates and returns a [Self] value.
|
||||
///
|
||||
pub fn init() Self {
|
||||
return Self{
|
||||
.filled = 0,
|
||||
.buffer = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
|
||||
/// stack is empty.
|
||||
|
@ -108,7 +98,7 @@ pub fn Fixed(comptime Element: type, comptime capacity: usize) type {
|
|||
|
||||
pub fn Unmanaged(comptime Element: type) type {
|
||||
return struct {
|
||||
filled: usize = 0,
|
||||
filled: usize,
|
||||
buffer: []Element,
|
||||
|
||||
const Self = @This();
|
||||
|
@ -147,16 +137,6 @@ pub fn Unmanaged(comptime Element: type) type {
|
|||
return self.filled;
|
||||
}
|
||||
|
||||
///
|
||||
/// Creates and returns a [Self] value wrapping `buffer` as its writable memory buffer.
|
||||
///
|
||||
pub fn init(buffer: []Element) Self {
|
||||
return Self{
|
||||
.filled = 0,
|
||||
.buffer = buffer,
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
|
||||
/// stack is empty.
|
||||
|
|
|
@ -0,0 +1,318 @@
|
|||
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 = "/",
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue