diff --git a/src/errors.zig b/src/errors.zig deleted file mode 100644 index 990bf77..0000000 --- a/src/errors.zig +++ /dev/null @@ -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; -} diff --git a/src/io.zig b/src/io.zig index 593d714..975aeee 100644 --- a/src/io.zig +++ b/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. diff --git a/src/main.zig b/src/main.zig index 7f7713c..8265a8e 100644 --- a/src/main.zig +++ b/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); } } diff --git a/src/mem.zig b/src/mem.zig new file mode 100644 index 0000000..4c489cb --- /dev/null +++ b/src/mem.zig @@ -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; +} diff --git a/src/stack.zig b/src/stack.zig index 507bfe8..3fc2de1 100755 --- a/src/stack.zig +++ b/src/stack.zig @@ -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. diff --git a/src/sys.zig b/src/sys.zig new file mode 100644 index 0000000..5bc25a2 --- /dev/null +++ b/src/sys.zig @@ -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 = "/", + }); + } +};