Experimental file system API changes

This commit is contained in:
kayomn 2022-09-27 21:45:32 +01:00
parent e3d51ae9c2
commit 3657b846f2
2 changed files with 106 additions and 256 deletions

View File

@ -8,97 +8,13 @@ const std = @import("std");
/// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a /// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a
/// given set of elements. /// given set of elements.
/// ///
pub const FinitePushError = error { pub const FixedPushError = error {
Overflow, Overflow,
}; };
/// pub fn Fixed(comptime Element: type) type {
/// Returns a fixed-size stack collection capable of holding a maximum of `capacity` elements of
/// type `Element`.
///
pub fn Fixed(comptime Element: type, comptime capacity: usize) type {
return struct { return struct {
filled: usize, filled: usize = 0,
buffer: [capacity]Element,
const Self = @This();
///
/// Wraps `self` and returns it in a [io.Writer] value.
///
/// Note that this will raise a compilation error if [Element] is not `u8`.
///
pub fn asWriter(self: *Self) io.Writer {
if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
@typeName(Element) ++ " into a Writer");
return io.Writer.wrap(Self, self, struct {
fn write(stack: *Self, buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
error.Overflow => return 0,
};
return buffer.len;
}
}.write);
}
///
/// Clears all elements from `self`.
///
pub fn clear(self: *Self) void {
self.filled = 0;
}
///
/// Counts and returns the number of pushed elements in `self`.
///
pub fn count(self: Self) usize {
return self.filled;
}
///
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
/// stack is empty.
///
pub fn pop(self: *Self) ?Element {
if (self.filled == 0) return null;
self.filled -= 1;
return self.buffer[self.filled];
}
///
/// Attempts to push `element` into `self`, returning [FinitePushError.Overflow] if the
/// stack is full.
///
pub fn push(self: *Self, element: Element) FinitePushError!void {
if (self.filled == capacity) return error.Overflow;
self.buffer[self.filled] = element;
self.filled += 1;
}
///
/// Attempts to push all of `elements` into `self`, returning [FinitePushError.Overflow] if
/// the stack does not have sufficient capacity to hold the new elements.
///
pub fn pushAll(self: *Self, elements: []const u8) FinitePushError!void {
const filled = (self.filled + elements.len);
if (filled > capacity) return error.Overflow;
std.mem.copy(u8, self.buffer[self.filled ..], elements);
self.filled = filled;
}
};
}
pub fn Unmanaged(comptime Element: type) type {
return struct {
filled: usize,
buffer: []Element, buffer: []Element,
const Self = @This(); const Self = @This();
@ -150,10 +66,9 @@ pub fn Unmanaged(comptime Element: type) type {
} }
/// ///
/// Attempts to push `element` into `self`, returning [FinitePushError.Overflow] if the /// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
/// stack is full.
/// ///
pub fn push(self: *Self, element: Element) FinitePushError!void { pub fn push(self: *Self, element: Element) FixedPushError!void {
if (self.filled == self.buffer.len) return error.Overflow; if (self.filled == self.buffer.len) return error.Overflow;
self.buffer[self.filled] = element; self.buffer[self.filled] = element;
@ -161,10 +76,10 @@ pub fn Unmanaged(comptime Element: type) type {
} }
/// ///
/// Attempts to push all of `elements` into `self`, returning [FinitePushError.Overflow] if /// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
/// the stack does not have sufficient capacity to hold the new elements. /// failed.
/// ///
pub fn pushAll(self: *Self, elements: []const u8) FinitePushError!void { pub fn pushAll(self: *Self, elements: []const u8) FixedPushError!void {
const filled = (self.filled + elements.len); const filled = (self.filled + elements.len);
if (filled > self.buffer.len) return error.Overflow; if (filled > self.buffer.len) return error.Overflow;
@ -180,7 +95,8 @@ test "fixed stack" {
const testing = @import("std").testing; const testing = @import("std").testing;
const expectError = testing.expectError; const expectError = testing.expectError;
const expectEqual = testing.expectEqual; const expectEqual = testing.expectEqual;
var stack = Fixed(u8, 4){}; var buffer = std.mem.zeroes([4]u8);
var stack = Fixed(u8){.buffer = &buffer};
try expectEqual(stack.count(), 0); try expectEqual(stack.count(), 0);
try expectEqual(stack.pop(), null); try expectEqual(stack.pop(), null);
@ -189,8 +105,8 @@ test "fixed stack" {
try expectEqual(stack.pop(), 69); try expectEqual(stack.pop(), 69);
try stack.pushAll(&.{42, 10, 95, 0}); try stack.pushAll(&.{42, 10, 95, 0});
try expectEqual(stack.count(), 4); try expectEqual(stack.count(), 4);
try expectError(FinitePushError.Overflow, stack.push(1)); try expectError(FixedPushError.Overflow, stack.push(1));
try expectError(FinitePushError.Overflow, stack.pushAll(&.{1, 11, 11})); try expectError(FixedPushError.Overflow, stack.pushAll(&.{1, 11, 11}));
stack.clear(); stack.clear();

View File

@ -2,6 +2,7 @@ const ext = @cImport({
@cInclude("SDL2/SDL.h"); @cInclude("SDL2/SDL.h");
}); });
const io = @import("./io.zig");
const mem = @import("./mem.zig"); const mem = @import("./mem.zig");
const stack = @import("./stack.zig"); const stack = @import("./stack.zig");
const std = @import("std"); const std = @import("std");
@ -12,15 +13,6 @@ const std = @import("std");
pub const EventLoop = packed struct { pub const EventLoop = packed struct {
current_request: ?*Request = null, 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 /// [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 /// failing to open a given file. This includes file-system restrictions surrounding a specific
@ -46,7 +38,7 @@ pub const EventLoop = packed struct {
/// such file exists, on the end of whatever it already contains. /// such file exists, on the end of whatever it already contains.
/// ///
pub const OpenMode = enum { pub const OpenMode = enum {
read, readonly,
overwrite, overwrite,
append, append,
}; };
@ -57,13 +49,14 @@ pub const EventLoop = packed struct {
message: union(enum) { message: union(enum) {
close: struct { close: struct {
file: *ext.SDL_RWops, file: *FileAccess,
}, },
open: struct { open: struct {
file_system: FileSystem,
path: *const Path, path: *const Path,
file: OpenError!*ext.SDL_RWops,
mode: OpenMode, mode: OpenMode,
file: OpenError!*FileAccess = error.OutOfFiles,
}, },
}, },
}; };
@ -82,44 +75,36 @@ pub const EventLoop = packed struct {
.message = .{.close = @ptrCast(*ext.SDL_RWops, file_access)}, .message = .{.close = @ptrCast(*ext.SDL_RWops, file_access)},
}; };
suspend { suspend if (event_loop.current_request) |current_request| {
if (event_loop.current_request) |current_request| { current_request.next = &request;
current_request.next = &request; } else {
} else { event_loop.current_request = &request;
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, file_system: FileSystem,
/// path: Path, mode: OpenMode) OpenError!*FileAccess {
pub fn open(event_loop: *EventLoop, path: Path, mode: OpenMode) OpenError!*FileAccess {
var request = Request{ var request = Request{
.frame = @frame(), .frame = @frame(),
.message = .{ .message = .{
.open = .{ .open = .{
.file_system = file_system,
.path = &path, .path = &path,
.file = error.OutOfFiles,
.mode = mode, .mode = mode,
}, },
}, },
}; };
suspend { suspend if (event_loop.current_request) |current_request| {
if (event_loop.current_request) |current_request| { current_request.next = &request;
current_request.next = &request; } else {
} else { event_loop.current_request = &request;
event_loop.current_request = &request; };
}
}
return @ptrCast(*FileAccess, try request.message.open.file); return @ptrCast(*FileAccess, try request.message.open.file);
} }
@ -132,15 +117,41 @@ pub const EventLoop = packed struct {
switch (request.message) { switch (request.message) {
.close => |*close| { .close => |*close| {
// Swallow file close errors. // Swallow file close errors.
_ = ext.SDL_RWclose(close.file); _ = ext.SDL_RWclose(@ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), close.file)));
}, },
.open => |*open| { .open => |*open| {
open.file = ext.SDL_RWFromFile(&open.path.buffer, switch (open.mode) { switch (open.file_system) {
.read => "rb", .data => {
.overwrite => "wb", var zeroed_path_buffer = std.mem.zeroes([4096]u8);
.append => "ab", var zeroed_path = stack.Fixed(u8){.buffer = &zeroed_path_buffer};
}) orelse error.NotFound;
zeroed_path.push('.') catch continue;
_ = open.path.write(zeroed_path.asWriter());
zeroed_path.push(0) catch continue;
// TODO: Access TAR file.
},
.user => {
var zeroed_path_buffer = std.mem.zeroes([4096]u8);
var zeroed_path = stack.Fixed(u8){.buffer = &zeroed_path_buffer};
zeroed_path.pushAll("/home/kayomn/.local/share") catch continue;
_ = open.path.write(zeroed_path.asWriter());
zeroed_path.push(0) catch continue;
open.file = @ptrCast(?*FileAccess, ext.SDL_RWFromFile(&zeroed_path_buffer, switch (open.mode) {
.readonly => "rb",
.overwrite => "wb",
.append => "ab",
})) orelse error.NotFound;
},
}
}, },
} }
@ -151,8 +162,26 @@ pub const EventLoop = packed struct {
} }
}; };
pub const FileAccess = opaque { ///
///
///
pub const FileAccess = opaque {};
///
/// 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,
};
///
///
///
pub const FileSystem = enum {
data,
user,
}; };
/// ///
@ -160,30 +189,13 @@ pub const FileAccess = opaque {
/// Ona. /// Ona.
/// ///
pub const Path = struct { pub const Path = struct {
locator: Locator,
length: u16, length: u16,
buffer: [max]u8, 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 Error = error {
pub const JoinError = error {
TooLong, TooLong,
}; };
@ -194,73 +206,11 @@ pub const Path = struct {
return (path.length == 0); 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`. /// Returns `true` if `this` is equal to `that`, otherwise `false`.
/// ///
pub fn equals(this: Path, that: Path) bool { pub fn equals(this: Path, that: Path) bool {
return (this.locator == that.locator) and return std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]);
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),
};
} }
/// ///
@ -273,46 +223,30 @@ pub const Path = struct {
pub const max = 1000; 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.) pub fn parseJoins(path: Path, joins: []const []const u8) Error!Path {
/// followed by the rest of the path. _ = path;
/// _ = joins;
/// * 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| { return Path{
var locator = std.meta.stringToEnum(Locator, .buffer = std.mem.zeroes([max]u8),
sequence[0 .. locator_path_delimiter_index]) orelse return error.BadLocator; .length = 0
};
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 = "/",
});
} }
///
///
///
pub fn write(path: Path, writer: io.Writer) bool {
return (writer.write(path.buffer[0 .. path.length]) == path.length);
}
///
///
///
pub const root = Path{
.buffer = [_]u8{'/'} ** max,
.length = 1,
};
}; };