diff --git a/src/core/io.zig b/src/core/io.zig index 79b4b73..bf794b8 100644 --- a/src/core/io.zig +++ b/src/core/io.zig @@ -4,31 +4,129 @@ const stack = @import("./stack.zig"); const testing = @import("./testing.zig"); /// -/// Allocation options for an [Allocator]. +/// [AccessError.Inacessible] is a generic catch-all for IO resources that are inaccessible for +/// implementation-specific reasons. /// -pub const Allocation = struct { - existing: ?[*]u8, - alignment: u29, - size: usize, +pub const AccessError = error { + Inaccessible, }; /// -/// Closure for dynamic memory allocation through the referenced allocator state machine capture. +/// [AllocationError.OutOfMemory] if the requested amount of memory could not be allocated. /// -pub const Allocator = meta.Function(Allocation, ?[*]u8); - -/// -/// [MakeError.OutOfMemory] if the requested amount of memory could not be allocated. -/// -pub const MakeError = error { +pub const AllocationError = error { OutOfMemory, }; /// -/// Closure that captures a reference to readable resources like block devices, memory buffers, -/// network sockets, and more. +/// Memory layout description for a memory allocation. /// -pub const Reader = meta.Function([]u8, usize); +pub const AllocationLayout = struct { + length: usize, + alignment: u29 = 8, +}; + +/// +/// Interface for dynamic memory allocation through the state machine of the wrapped allocator +/// implementation. +/// +pub const Allocator = struct { + context: *anyopaque, + + vtable: *const struct { + alloc: fn (*anyopaque, AllocationLayout) AllocationError![*]u8, + dealloc: fn (*anyopaque, [*]u8) void, + realloc: fn (*anyopaque, [*]u8, AllocationLayout) AllocationError![*]u8, + }, + + /// + /// Attempts to allocate a block of memory from `allocator` according to `layout`, returning it + /// or [AllocationError] if it failed. + /// + pub fn alloc(allocator: Allocator, layout: AllocationLayout) AllocationError![*]u8 { + return allocator.vtable.alloc(allocator.context, layout); + } + + /// + /// Deallocates the block of memory from `allocator` referenced by `allocation`. + /// + pub fn dealloc(allocator: Allocator, allocation: [*]u8) void { + allocator.vtable.dealloc(allocator.context, allocation); + } + + /// + /// Attempts to reallocate the existing block of memory from `allocator` referenced by + /// `allocation` according to `layout`, returning it or [AllocationError] if it failed. + /// + pub fn realloc(allocator: Allocator, allocation: [*]u8, + layout: AllocationLayout) AllocationError![*]u8 { + + return allocator.vtable.realloc(allocator.context, allocation, layout); + } + + /// + /// Wraps `implementation`, returning the [Allocator] value. + /// + pub fn wrap(implementation: anytype) Allocator { + const Implementation = @TypeOf(implementation.*); + + return .{ + .context = @ptrCast(*anyopaque, implementation), + + .vtable = switch (@typeInfo(Implementation)) { + .Struct => &.{ + .alloc = struct { + fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 { + return @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).alloc(layout); + } + }.call, + + .dealloc = struct { + fn call(context: *anyopaque, allocation: [*]u8) void { + return @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).dealloc(allocation); + } + }.call, + + .realloc = struct { + fn call(context: *anyopaque, allocation: [*]u8, + layout: AllocationLayout) AllocationError![*]u8 { + + return @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).realloc(allocation, layout); + } + }.call, + }, + + .Opaque => &.{ + .alloc = struct { + fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 { + return @ptrCast(*Implementation, context).alloc(layout); + } + }.call, + + .dealloc = struct { + fn call(context: *anyopaque, allocation: [*]u8) void { + return @ptrCast(*Implementation, context).dealloc(allocation); + } + }.call, + + .realloc = struct { + fn call(context: *anyopaque, allocation: [*]u8, + layout: AllocationLayout) AllocationError![*]u8 { + + return @ptrCast(*Implementation, context).realloc(allocation, layout); + } + }.call, + }, + + else => @compileError( + "`context` must a single-element pointer referencing a struct or opaque type"), + }, + }; + } +}; /// /// Returns a state machine for lazily computing all `Element` components of a given source input @@ -143,10 +241,57 @@ test "Spliterator(u8)" { } /// -/// Closure that captures a reference to writable resources like block devices, memory buffers, +/// Interface for capturing a reference to a writable resource like block devices, memory buffers, /// network sockets, and more. /// -pub const Writer = meta.Function([]const u8, usize); +pub const Writer = struct { + context: *anyopaque, + + vtable: *const struct { + write: fn (*anyopaque, []const u8) AccessError!usize, + }, + + /// + /// Wraps `implementation`, returning the [Writer] value. + /// + pub fn wrap(implementation: anytype) Writer { + const Implementation = @TypeOf(implementation.*); + + return .{ + .context = @ptrCast(*anyopaque, implementation), + + .vtable = switch (@typeInfo(Implementation)) { + .Struct => &.{ + .write = struct { + fn call(context: *anyopaque, buffer: []const u8) AccessError!usize { + return @ptrCast(*Implementation, + @alignCast(@alignOf(Implementation), context)).write(buffer); + } + }.call, + }, + + .Opaque => &.{ + .write = struct { + fn call(context: *anyopaque, buffer: []const u8) AccessError!usize { + return @ptrCast(*Implementation, context).write(buffer); + } + }.call, + }, + + else => @compileError( + "`context` must a single-element pointer referencing a struct or opaque type"), + }, + }; + } + + /// + /// Attempts to write to `buffer` to `writer`, returning the number of successfully written or + /// [AccessError] if it failed. + /// + pub fn write(writer: Writer, buffer: []const u8) AccessError!usize { + return writer.vtable.write(writer.context, buffer); + } +}; /// /// Returns a sliced reference of the raw bytes in `pointer`. @@ -320,19 +465,14 @@ test "findFirstOf" { /// `allocated_memory`. Anything else will result is considered unreachable logic. /// pub fn free(allocator: Allocator, allocated_memory: anytype) void { - if (allocator.call(.{ - .existing = @ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) { - .Pointer => |info| switch (info.size) { - .One, .Many, .C => allocated_memory, - .Slice => allocated_memory.ptr, - }, + allocator.dealloc(@ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) { + .Pointer => |info| switch (info.size) { + .One, .Many, .C => allocated_memory, + .Slice => allocated_memory.ptr, + }, - else => @compileError("`allocated_memory` must be a pointer"), - }), - - .size = 0, - .alignment = 0, - }) != null) unreachable; + else => @compileError("`allocated_memory` must be a pointer"), + })); } test "free" { @@ -369,14 +509,13 @@ test "hashBytes" { /// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a /// [MakeError] if it failed. /// -pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) MakeError![*]Element { +pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) AllocationError![*]Element { const alignment = @alignOf(Element); - return @ptrCast([*]Element, @alignCast(alignment, allocator.call(.{ - .existing = null, - .size = @sizeOf(Element) * size, + return @ptrCast([*]Element, @alignCast(alignment, try allocator.alloc(.{ + .length = @sizeOf(Element) * size, .alignment = alignment, - }) orelse return error.OutOfMemory)); + }))); } test "makeMany" { @@ -392,14 +531,13 @@ test "makeMany" { /// Attempts to allocate a buffer of `1` `Element` using `allocator`, returning it or a [MakeError] /// if it failed. /// -pub fn makeOne(comptime Element: type, allocator: Allocator) MakeError!*Element { +pub fn makeOne(comptime Element: type, allocator: Allocator) AllocationError!*Element { const alignment = @alignOf(Element); - return @ptrCast(*Element, @alignCast(alignment, allocator.call(.{ - .existing = null, - .size = @sizeOf(Element), + return @ptrCast(*Element, @alignCast(alignment, try allocator.alloc(.{ + .length = @sizeOf(Element), .alignment = alignment, - }) orelse return error.OutOfMemory)); + }))); } test "makeOne" { @@ -429,6 +567,11 @@ test "swap" { try testing.expect(b == 0); } +/// +/// Mandatory context variable used by [null_writer]. +/// +const null_context: u64 = 0; + /// /// Thread-safe and lock-free [Writer] that silently consumes all given data without failure and /// throws it away. @@ -436,11 +579,13 @@ test "swap" { /// This is commonly used for testing or redirected otherwise unwanted output data that has to be /// sent somewhere for whatever reason. /// -pub const null_writer = Writer.from(struct { - fn write(buffer: []const u8) usize { +pub const null_writer = Writer.wrap(@ptrCast(*const opaque { + const Self = @This(); + + fn write(_: Self, buffer: []const u8) usize { return buffer.len; } -}.write); +}, &null_context)); test "null_writer" { const sequence = "foo"; diff --git a/src/core/meta.zig b/src/core/meta.zig index ce54238..9fa2c48 100644 --- a/src/core/meta.zig +++ b/src/core/meta.zig @@ -8,77 +8,3 @@ pub fn FnReturn(comptime Fn: type) type { return type_info.Fn.return_type orelse void; } - -/// -/// Returns a single-input single-output closure type where `In` represents the input type, `Out` -/// represents the output type, and `captures_size` represents the size of the closure context. -/// -pub fn Function(comptime In: type, comptime Out: type) type { - return struct { - callErased: fn (*anyopaque, In) Out, - context: *anyopaque, - - fn Invoker(comptime Context: type) type { - return if (Context == void) fn (In) Out else fn (Context, In) Out; - } - - /// - /// Function type. - /// - const Self = @This(); - - /// - /// Invokes `self` with `input`, producing a result according to the current context data. - /// - pub fn call(self: Self, input: In) Out { - return self.callErased(self.context, input); - } - - /// - /// Creates and returns a [Self] using the `invoke` as the behavior executed when [call] or - /// [callErased] is called. - /// - /// For creating a closure-style function, see [fromClosure]. - /// - pub fn from(comptime invoke: fn (In) Out) Self { - return .{ - .context = undefined, - - .callErased = struct { - fn callErased(_: *anyopaque, input: In) Out { - return invoke(input); - } - }.callErased, - }; - } - - /// - /// Creates and returns a [Self] by capturing the `context` value as the capture context and - /// `invoke` as the behavior executed when [call] or [callErased] is called. - /// - /// The newly created [Self] is returned. - /// - pub fn fromClosure(context: anytype, comptime invoke: fn (@TypeOf(context), In) Out) Self { - const Context = @TypeOf(context); - - switch (@typeInfo(Context)) { - .Pointer => |info| if (info.size == .Slice) - @compileError("`context` cannot be a slice"), - - .Void => {}, - else => @compileError("`context` must be a pointer"), - } - - return Self{ - .context = @ptrCast(*anyopaque, context), - - .callErased = struct { - fn callErased(erased: *anyopaque, input: In) Out { - return if (Context == void) invoke(input) else invoke(@ptrCast( - Context, @alignCast(@alignOf(Context), erased)), input); - } - }.callErased, - }; - } - }; -} diff --git a/src/core/stack.zig b/src/core/stack.zig index 609d66c..7cec654 100755 --- a/src/core/stack.zig +++ b/src/core/stack.zig @@ -145,38 +145,69 @@ pub const FixedPushError = error { /// memory pool to linearly allocate memory from. /// pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator { - return io.Allocator.fromClosure(fixed_stack, struct { - fn alloc(stack: *Fixed(u8), allocation: io.Allocation) ?[*]u8 { - if (allocation.existing) |buffer| if (allocation.size == 0) { - // Deallocate the memory. - const buffer_address = @ptrToInt(buffer); - const stack_address = @ptrToInt(stack.buffer.ptr); + const FixedStack = @TypeOf(fixed_stack.*); - // Check the buffer is within the address space of the stack buffer. If not, it - // should just be returned to let the caller know it cannot be freed. - if (buffer_address < stack_address or buffer_address >= - (stack_address + stack.filled)) return buffer; + return io.Allocator.wrap(@ptrCast(*opaque { + const Self = @This(); - // TODO: Investigate ways of actually freeing if it is the last allocation. - return null; - } else { - // TODO: Investigate ways of in-place relocating if it is the last allocation. - }; - - // Reallocate / allocate the memory. + pub fn alloc(self: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 { // TODO: Remove stdlib dependency. + const stack = self.stackCast(); + const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr + - stack.filled, allocation.alignment) orelse return null; + stack.filled, layout.alignment) orelse return error.OutOfMemory; const head = stack.filled + adjusted_offset; - const tail = head + allocation.size; + const tail = head + layout.length; - stack.pushMany(0, tail) catch return null; + stack.pushMany(0, tail) catch return error.OutOfMemory; return stack.buffer[head .. tail].ptr; - } - }.alloc); + + pub fn dealloc(self: *Self, allocation: [*]u8) void { + // Deallocate the memory. + const stack = self.stackCast(); + const allocation_address = @ptrToInt(allocation); + const stack_address = @ptrToInt(stack.buffer.ptr); + + // Check the buffer is within the address space of the stack buffer. If not, it cannot + // be freed. + if (allocation_address < stack_address or allocation_address >= + (stack_address + stack.filled)) unreachable; + + // TODO: Investigate ways of actually freeing if it is the last allocation. + } + + pub fn realloc(self: *Self, allocation: [*]u8, + layout: io.AllocationLayout) io.AllocationError![*]u8 { + + // TODO: Investigate ways of in-place relocating if it is the last allocation. + // TODO: Remove stdlib dependency. + const stack = self.stackCast(); + const allocation_address = @ptrToInt(allocation); + const stack_address = @ptrToInt(stack.buffer.ptr); + + // Check the buffer is within the address space of the stack buffer. If not, it cannot + // be reallocated. + if (allocation_address < stack_address or allocation_address >= + (stack_address + stack.filled)) unreachable; + + const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr + + stack.filled, layout.alignment) orelse return error.OutOfMemory; + + const head = stack.filled + adjusted_offset; + const tail = head + layout.length; + + stack.pushMany(0, tail) catch return error.OutOfMemory; + + return stack.buffer[head .. tail].ptr; + } + + fn stackCast(self: *Self) *Fixed(u8) { + return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self)); + } + }, fixed_stack)); } test "fixedAllocator" { @@ -185,14 +216,11 @@ test "fixedAllocator" { const allocator = fixedAllocator(&stack); // Allocation - var block_memory = allocator.call(.{ - .existing = null, + var block_memory = try allocator.alloc(.{ .alignment = @alignOf(u64), - .size = @sizeOf(u64), + .length = @sizeOf(u64), }); - try testing.expect(block_memory != null); - const buffer_address_head = @ptrToInt(&buffer); const buffer_address_tail = @ptrToInt(&buffer) + buffer.len; @@ -204,14 +232,11 @@ test "fixedAllocator" { } // Reallocation. - block_memory = allocator.call(.{ - .existing = block_memory, + block_memory = try allocator.realloc(block_memory, .{ .alignment = @alignOf(u64), - .size = @sizeOf(u64), + .length = @sizeOf(u64), }); - try testing.expect(block_memory != null); - { const block_memory_address = @ptrToInt(block_memory); @@ -220,11 +245,7 @@ test "fixedAllocator" { } // Deallocation. - try testing.expect(allocator.call(.{ - .existing = block_memory, - .alignment = 0, - .size = 0, - }) == null); + allocator.dealloc(block_memory); } /// @@ -234,15 +255,23 @@ test "fixedAllocator" { /// referenced by `fixed_stack` until it is full. /// pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer { - return io.Writer.fromClosure(fixed_stack, struct { - fn write(stack: *Fixed(u8), buffer: []const u8) usize { - stack.pushAll(buffer) catch |err| switch (err) { + const FixedStack = @TypeOf(fixed_stack.*); + + return io.Writer.wrap(@ptrCast(*opaque { + const Self = @This(); + + fn stackCast(self: *Self) *Fixed(u8) { + return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self)); + } + + pub fn write(self: *Self, buffer: []const u8) io.AccessError!usize { + self.stackCast().pushAll(buffer) catch |err| switch (err) { error.BufferOverflow => return 0, }; return buffer.len; } - }.write); + }, fixed_stack)); } test "fixedWriter" { @@ -250,6 +279,8 @@ test "fixedWriter" { var sequence_stack = Fixed(u8){.buffer = &buffer}; const sequence_data = [_]u8{8, 16, 32, 64}; - try testing.expect(fixedWriter(&sequence_stack).call(&sequence_data) == sequence_data.len); + try testing.expect((try fixedWriter(&sequence_stack). + write(&sequence_data)) == sequence_data.len); + try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data)); } diff --git a/src/core/table.zig b/src/core/table.zig index 8cfb978..174449a 100644 --- a/src/core/table.zig +++ b/src/core/table.zig @@ -29,6 +29,11 @@ pub fn Hashed(comptime Key: type, comptime Value: type, maybe_next_index: ?usize = null, }; + /// + /// Errors that may occur during initialization of a hash table. + /// + pub const InitError = io.AllocationError; + /// /// Hash table type. /// @@ -46,9 +51,9 @@ pub fn Hashed(comptime Key: type, comptime Value: type, /// /// Initializes a [Self] using `allocator` as the memory allocation strategy. /// - /// Returns a new [Self] value or an [io.MakeError] if initializing failed. + /// Returns a new [Self] value or an [InitError] if initializing failed. /// - pub fn init(allocator: Allocator) io.MakeError!Self { + pub fn init(allocator: Allocator) InitError!Self { const capacity = 4; return Self{ @@ -166,7 +171,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, /// [InsertError.KeyExists] occurs when an insertion was attempted on a table with a matching key /// already present. /// -pub const InsertError = io.MakeError || error { +pub const InsertError = io.AllocationError || error { KeyExists, }; diff --git a/src/core/unicode.zig b/src/core/unicode.zig index de6279f..13e9a21 100644 --- a/src/core/unicode.zig +++ b/src/core/unicode.zig @@ -7,7 +7,7 @@ const testing = @import("./testing.zig"); /// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write /// the entirety of a the requested print operation. /// -pub const PrintError = error { +pub const PrintError = io.AccessError || error { WriteFailure, }; @@ -69,7 +69,7 @@ pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void if (value == 0) { const zero = "0"; - if (writer.call(zero) != zero.len) return error.WriteFailure; + if ((try writer.write(zero)) != zero.len) return error.WriteFailure; } else { // Big enough to hold the hexadecimal representation of the integer type, which is // the largest number format accomodated for in [Radix]. @@ -95,7 +95,7 @@ pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void for (buffer[0 .. (buffer_count / 2)]) |_, i| io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]); - if (writer.call(buffer[0 .. buffer_count]) != buffer_count) + if ((try writer.write(buffer[0 .. buffer_count])) != buffer_count) return error.WriteFailure; } }, diff --git a/src/ona/main.zig b/src/ona/main.zig index 0045c34..6bf9f79 100644 --- a/src/ona/main.zig +++ b/src/ona/main.zig @@ -13,22 +13,19 @@ pub fn main() anyerror!void { /// Runs the game engine. /// fn runEngine(app: *sys.App, graphics: *sys.Graphics) anyerror!void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - - defer _ = gpa.deinit(); - { - var file_reader = try app.data.openRead(try sys.Path.from(&.{"ona.lua"})); + const path = try sys.Path.from(&.{"ona.lua"}); + var file_reader = try app.data.openFileReader(path); defer file_reader.close(); - const file_size = try file_reader.size(); - const allocator = gpa.allocator(); - const buffer = try allocator.alloc(u8, file_size); + const file_size = (try app.data.query(path)).length; + const allocator = sys.threadSafeAllocator(); + const buffer = (try core.io.makeMany(u8, allocator, file_size))[0 .. file_size]; - defer allocator.free(buffer); + defer core.io.free(allocator, buffer); - if ((try file_reader.read(0, buffer)) != file_size) return error.ScriptLoadFailure; + if ((try file_reader.read(buffer)) != file_size) return error.ScriptLoadFailure; app.log(.debug, buffer); } diff --git a/src/ona/oar.zig b/src/ona/oar.zig index 05ee2df..d22b7ae 100644 --- a/src/ona/oar.zig +++ b/src/ona/oar.zig @@ -2,13 +2,13 @@ const core = @import("core"); const sys = @import("./sys.zig"); /// -/// Metadata of an Oar archive entry. +/// Metadata of an Oar archive file entry. /// -const Block = extern struct { +const Entry = extern struct { signature: [signature_magic.len]u8 = signature_magic, path: sys.Path = sys.Path.empty, - data_size: u64 = 0, - data_head: u64 = 0, + data_offset: u64 = 0, + data_length: u64 = 0, padding: [232]u8 = [_]u8{0} ** 232, comptime { @@ -19,84 +19,14 @@ const Block = extern struct { }; /// -/// Reference to a file entry in an Oar archive, denoting the starting offset from the top of head -/// of the file and its size. +/// [FindError.ArchiveUnsupported] occurs when trying to read a file that does not follow an Oar +/// archive format considered valid by this implemenatation. /// -pub const Entry = struct { - head: u64, - size: u64, - - /// - /// [FindError.EntryNotFound] occurs when no entry matched the parameters of the find operation. - /// - /// [FindError.ArchiveUnsupported] occurs if the file provided to the find operation is not a - /// valid archive file. - /// - pub const FindError = error { - EntryNotFound, - ArchiveUnsupported, - }; - - /// - /// Attempts to perform a binary search on the entry blocks defined in `archive_file` for one - /// matching `entry_path`, returning an [Entry] referencing its data or a [FindError] if it - /// failed. - /// - /// **Note** that this operation has `O(log n)` time complexity. - /// - pub fn find(archive_file: *sys.ReadableFile, entry_path: sys.Path) FindError!Entry { - var header = Header{}; - const header_size = @sizeOf(Header); - const io = core.io; - - if (((archive_file.read(0, io.bytesOf(&header)) catch - return error.ArchiveUnsupported) != header_size) or - (!io.equals(u8, &header.signature, &signature_magic)) or - (header.revision != revision_magic) or - (header.entry_head <= header_size)) return error.ArchiveUnsupported; - - // Read file table. - var head: usize = 0; - var tail: usize = (header.entry_count - 1); - const block_size = @sizeOf(Block); - - while (head <= tail) { - var block = Block{}; - const midpoint = (head + (tail - head) / 2); - - if ((archive_file.read(header.entry_head + (block_size * midpoint), io.bytesOf(&block)) - catch return error.ArchiveUnsupported) != block_size) return error.EntryNotFound; - - const comparison = entry_path.compare(block.path); - - if (comparison == 0) return Entry{ - .head = block.data_head, - .size = block.data_size, - }; - - if (comparison > 0) { - head = (midpoint + 1); - } else { - tail = (midpoint - 1); - } - } - - return error.EntryNotFound; - } - - /// - /// Reads the data from `entry` in `archive_file` from the byte at the entry-relative `offset` - /// into `buffer` until either the end of the entry data, end of archive file, or end of buffer - /// is reached. - /// - /// The number of bytes read is returned or [sys.FileError] if it failed. - /// - pub fn read(entry: Entry, archive_file: *sys.ReadableFile, - offset: u64, buffer: []u8) sys.FileError!usize { - - return archive_file.read(entry.head + offset, - buffer[0 .. core.math.min(usize, buffer.len, entry.size)]); - } +/// [FindError.EntryNotFound] occurs when the queried entry was not found in the archive file. +/// +pub const FindError = core.io.AccessError || error { + ArchiveUnsupported, + EntryNotFound, }; /// @@ -106,8 +36,7 @@ const Header = extern struct { signature: [signature_magic.len]u8 = signature_magic, revision: u8 = revision_magic, entry_count: u32 = 0, - entry_head: u64 = 0, - padding: [496]u8 = [_]u8{0} ** 496, + padding: [502]u8 = [_]u8{0} ** 502, comptime { const size = @sizeOf(@This()); @@ -117,7 +46,54 @@ const Header = extern struct { }; /// -/// The magic revision number that this Oar software implementation understands. +/// Attempts to find an [Entry] with a path name matching `path` in `archive_reader`. +/// +/// An [Entry] value is returned if a match was found, otherwise [FindError] if it failed. +/// +pub fn findEntry(archive_reader: sys.FileReader, path: sys.Path) FindError!Entry { + var header = Header{}; + const header_size = @sizeOf(Header); + const io = core.io; + + if ((try archive_reader.read(io.bytesOf(&header))) != header_size) + return error.ArchiveUnsupported; + + if (!io.equals(u8, &header.signature, &signature_magic)) + return error.ArchiveUnsupported; + + if (header.revision != revision_magic) return error.ArchiveUnsupported; + + // Read file table. + var head: u64 = 0; + var tail: u64 = (header.entry_count - 1); + const entry_size = @sizeOf(Entry); + + while (head <= tail) { + var entry = Entry{}; + const midpoint = head + ((tail - head) / 2); + const offset = header_size + (entry_size * midpoint); + + try archive_reader.seek(offset); + + if ((try archive_reader.read(io.bytesOf(&entry))) != entry_size) + return error.ArchiveUnsupported; + + const comparison = path.compare(entry.path); + + if (comparison == 0) return entry; + + if (comparison > 0) { + head = (midpoint + 1); + } else { + tail = (midpoint - 1); + } + } + + return error.EntryNotFound; +} + +/// +/// Magic revision number that this Oar software implementation understands. /// const revision_magic = 0; diff --git a/src/ona/sys.zig b/src/ona/sys.zig index 4682760..c5b66a0 100644 --- a/src/ona/sys.zig +++ b/src/ona/sys.zig @@ -93,86 +93,106 @@ pub const App = struct { }; /// +/// Snapshotted information about the status of a file. /// -/// -pub const ReadableFile = opaque { - /// - /// - /// - pub fn close(readable_file: *ReadableFile) void { - if (ext.SDL_RWclose(readable_file.rwOpsCast()) != 0) - return ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, - "Attempt to close an invalid file reference"); - } - - /// - /// - /// - pub fn read(readable_file: *ReadableFile, offset: u64, buffer: []u8) FileError!u64 { - const rw_ops = readable_file.rwOpsCast(); - - { - ext.SDL_ClearError(); - - const math = core.math; - const min = math.min; - const maxIntValue = math.maxIntValue; - var sought = min(u64, offset, maxIntValue(i64)); - - if (ext.SDL_RWseek(rw_ops, @intCast(i64, sought), ext.RW_SEEK_SET) < 0) - return error.FileInaccessible; - - var to_seek = offset - sought; - - while (to_seek != 0) { - sought = min(u64, to_seek, maxIntValue(i64)); - - ext.SDL_ClearError(); - - if (ext.SDL_RWseek(rw_ops, @intCast(i64, sought), ext.RW_SEEK_CUR) < 0) - return error.FileInaccessible; - - to_seek -= sought; - } - } - - ext.SDL_ClearError(); - - const buffer_read = ext.SDL_RWread(rw_ops, buffer.ptr, @sizeOf(u8), buffer.len); - - if ((buffer_read == 0) and (ext.SDL_GetError() != null)) - return error.FileInaccessible; - - return buffer_read; - } - - /// - /// - /// - pub fn rwOpsCast(readable_file: *ReadableFile) *ext.SDL_RWops { - return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), readable_file)); - } - - /// - /// - /// - pub fn size(readable_file: *ReadableFile) FileError!u64 { - ext.SDL_ClearError(); - - const byte_size = ext.SDL_RWsize(readable_file.rwOpsCast()); - - if (byte_size < 0) return error.FileInaccessible; - - return @intCast(u64, byte_size); - } +pub const FileStatus = struct { + length: u64, }; /// -/// [Error.FileInaccessible] is a generic catch-all for a [FileAccess] reference no longer pointing -/// to a file or the file becomming invalid for whatever reason. +/// Interface for working with bi-directional, streamable resources accessed through a file-system. /// -pub const FileError = error { - FileInaccessible, +pub const FileReader = struct { + context: *anyopaque, + + vtable: *const struct { + close: fn (*anyopaque) void, + read: fn (*anyopaque, []u8) core.io.AccessError!u64, + seek: fn (*anyopaque, u64) core.io.AccessError!void, + }, + + /// + /// Closes the `file_reader`, logging a wraning if the `file_reader` is already considered + /// closed. + /// + pub fn close(file_reader: FileReader) void { + file_reader.vtable.close(file_reader.context); + } + + /// + /// Attempts to read from `file_reader` into `buffer`, returning the number of bytes + /// successfully read or [core.io.AccessError] if it failed. + /// + pub fn read(file_reader: FileReader, buffer: []u8) core.io.AccessError!u64 { + return file_reader.vtable.read(file_reader.context, buffer); + } + + /// + /// Attempts to seek from the beginning of `file_reader` to `cursor` bytes in, returning + /// [core.io.AccessError] if it failed. + /// + pub fn seek(file_reader: FileReader, cursor: u64) core.io.AccessError!void { + return file_reader.vtable.seek(file_reader.context, cursor); + } + + /// + /// Wraps `implementation`, returning a [FileReader] value. + /// + pub fn wrap(implementation: anytype) FileReader { + const Implementation = @TypeOf(implementation.*); + + return .{ + .context = @ptrCast(*anyopaque, implementation), + + .vtable = switch (@typeInfo(Implementation)) { + .Struct => &.{ + .close = struct { + fn call(context: *anyopaque) void { + @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).close(); + } + }.call, + + .read = struct { + fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 { + return @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).read(buffer); + } + }.call, + + .seek = struct { + fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void { + return @ptrCast(*Implementation, @alignCast( + @alignOf(Implementation), context)).seek(cursor); + } + }.call, + }, + + .Opaque => &.{ + .close = struct { + fn call(context: *anyopaque) void { + @ptrCast(*Implementation, context).close(); + } + }.call, + + .read = struct { + fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 { + return @ptrCast(*Implementation, context).read(buffer); + } + }.call, + + .seek = struct { + fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void { + return @ptrCast(*Implementation, context).seek(cursor); + } + }.call, + }, + + else => @compileError( + "`context` must a single-element pointer referencing a struct or opaque type"), + }, + }; + } }; /// @@ -181,42 +201,84 @@ pub const FileError = error { /// pub const FileSystem = union(enum) { native: []const u8, - archive_file: *ReadableFile, + + archive: struct { + file_system: *const FileSystem, + path: Path, + }, /// - /// 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 - /// [OpenError.FileNotFound] errors. + /// [AccessError.FileNotFound] occurs when a queried file could not be found on the file-system + /// by the process. This may mean the file does not exist, however it may also mean that the + /// process does not have sufficient rights to read it. /// - /// When a given [FileSystem] does not support a specified [OpenMode], - /// [OpenError.ModeUnsupported] is used to inform the consuming code that another [OpenMode] - /// should be tried or, if no mode other is suitable, that the resource is effectively - /// unavailable. + /// [AccessError.FileSystemfailure] denotes a file-system implementation-specific failure to + /// access resources has occured and therefore cannot proceed to access the file. /// - /// If the number of known [FileAccess] handles has been exhausted, [OpenError.OutOfFiles] is - /// used to communicate this. - /// - pub const OpenError = error { + pub const AccessError = error { FileNotFound, - ModeUnsupported, - OutOfFiles, + FileSystemFailure, }; /// - /// Attempts to open the file identified by `path` with `mode` as the mode for opening the file. + /// Attempts to open the file identified by `path` on `file_system` for reading, returning a + /// [FileReader] value that provides access to the opened file or [AccessError] if it failed. /// - /// Returns a [ReadableFile] reference that provides access to the file referenced by `path`or a - /// [OpenError] if it failed. - /// - pub fn openRead(file_system: *const FileSystem, path: Path) OpenError!*ReadableFile { - switch (file_system.*) { - .archive_file => |archive_file| { - const entry = oar.Entry.find(archive_file, path) catch return error.FileNotFound; + pub fn openFileReader(file_system: FileSystem, path: Path) AccessError!FileReader { + switch (file_system) { + .archive => |archive| { + const archive_reader = try archive.file_system.openFileReader(archive.path); - _ = entry; - // TODO: Alloc file context. + errdefer archive_reader.close(); - return error.FileNotFound; + const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) { + error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure, + error.EntryNotFound => error.FileNotFound, + }; + + archive_reader.seek(entry.data_offset) catch return error.FileSystemFailure; + + const io = core.io; + + const allocator = threadSafeAllocator(); + + const entry_reader = io.makeOne(struct { + allocator: io.Allocator, + base_reader: FileReader, + cursor: u64, + offset: u64, + length: u64, + + const Self = @This(); + + pub fn close(self: *Self) void { + self.base_reader.close(); + io.free(self.allocator, self); + } + + pub fn read(self: *Self, buffer: []u8) io.AccessError!u64 { + try self.base_reader.seek(self.offset + self.cursor); + + return self.base_reader.read(buffer[0 .. + core.math.min(usize, buffer.len, self.length)]); + } + + pub fn seek(self: *Self, cursor: u64) io.AccessError!void { + self.cursor = cursor; + } + }, allocator) catch return error.FileSystemFailure; + + errdefer io.free(allocator, entry_reader); + + entry_reader.* = .{ + .allocator = allocator, + .base_reader = archive_reader, + .cursor = 0, + .offset = entry.data_offset, + .length = entry.data_length, + }; + + return FileReader.wrap(entry_reader); }, .native => |native| { @@ -239,8 +301,124 @@ pub const FileSystem = union(enum) { ext.SDL_ClearError(); - return @ptrCast(*ReadableFile, ext.SDL_RWFromFile(&path_buffer, "rb") - orelse return error.FileNotFound); + const rw_ops = + ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileNotFound; + + errdefer _ = ext.SDL_RWclose(rw_ops); + + return FileReader.wrap(@ptrCast(*opaque { + const Self = @This(); + + fn rwOpsCast(self: *Self) *ext.SDL_RWops { + return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), self)); + } + + pub fn read(self: *Self, buffer: []u8) core.io.AccessError!u64 { + ext.SDL_ClearError(); + + const bytes_read = + ext.SDL_RWread(self.rwOpsCast(), buffer.ptr, @sizeOf(u8), buffer.len); + + if ((bytes_read == 0) and (ext.SDL_GetError() != null)) + return error.Inaccessible; + + return bytes_read; + } + + pub fn seek(self: *Self, cursor: u64) core.io.AccessError!void { + ext.SDL_ClearError(); + + const math = core.math; + const min = math.min; + const maxIntValue = math.maxIntValue; + var sought = min(u64, cursor, maxIntValue(i64)); + const ops = self.rwOpsCast(); + + if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_SET) < 0) + return error.Inaccessible; + + var to_seek = cursor - sought; + + while (to_seek != 0) { + sought = min(u64, to_seek, maxIntValue(i64)); + + ext.SDL_ClearError(); + + if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_CUR) < 0) + return error.Inaccessible; + + to_seek -= sought; + } + } + + pub fn close(self: *Self) void { + ext.SDL_ClearError(); + + if (ext.SDL_RWclose(self.rwOpsCast()) != 0) + return ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, + "Attempt to close an invalid file reference"); + } + }, rw_ops)); + }, + } + } + + /// + /// Attempts to query the status of the file identified by `path` on `file_system` for reading, + /// returning a [FileStatus] value containing a the state of the file at the moment or + /// [AccessError] if it failed. + /// + pub fn query(file_system: FileSystem, path: Path) AccessError!FileStatus { + switch (file_system) { + .archive => |archive| { + const archive_reader = try archive.file_system.openFileReader(archive.path); + + defer archive_reader.close(); + + const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) { + error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure, + error.EntryNotFound => error.FileNotFound, + }; + + return FileStatus{ + .length = entry.data_length, + }; + }, + + .native => |native| { + if (native.len == 0) return error.FileNotFound; + + var path_buffer = [_]u8{0} ** 4096; + const seperator_length = @boolToInt(native[native.len - 1] != Path.seperator); + + if ((native.len + seperator_length + path.length) >= path_buffer.len) + return error.FileNotFound; + + const io = core.io; + + io.copy(u8, &path_buffer, native); + + if (seperator_length != 0) path_buffer[native.len] = Path.seperator; + + io.copy(u8, path_buffer[native.len .. path_buffer.len], + path.buffer[0 .. path.length]); + + ext.SDL_ClearError(); + + const rw_ops = + ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileSystemFailure; + + defer if (ext.SDL_RWclose(rw_ops) != 0) unreachable; + + ext.SDL_ClearError(); + + const size = ext.SDL_RWsize(rw_ops); + + if (size < 0) return error.FileSystemFailure; + + return FileStatus{ + .length = @intCast(u64, size), + }; }, } } @@ -346,7 +524,8 @@ pub const Path = extern struct { }; /// - /// + /// Returns a value above `0` if the path of `this` is greater than `that`, below `0` if it is + /// less, or `0` if they are identical. /// pub fn compare(this: Path, that: Path) isize { return core.io.compareBytes(this.buffer[0 ..this.length], that.buffer[0 .. that.length]); @@ -432,16 +611,29 @@ pub const RunError = error { }; /// -/// Returns a [core.io.Allocator] bound to the underlying system allocator. +/// Returns a thread-safe [core.io.Allocator] value based on the default system allocation strategy. /// -pub fn allocator() core.io.Allocator { - // TODO: Add leak detection. - return .{ - .bound = .{ - .alloc = ext.SDL_alloc, - .dealloc = ext.SDL_free, - }, - }; +pub fn threadSafeAllocator() core.io.Allocator { + const io = core.io; + + return io.Allocator.wrap(@as(*opaque { + const Self = @This(); + + pub fn alloc(_: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 { + return @ptrCast([*]u8, ext.SDL_malloc(layout.length) orelse return error.OutOfMemory); + } + + pub fn realloc(_: *Self, allocation: [*]u8, + layout: io.AllocationLayout) io.AllocationError![*]u8 { + + return @ptrCast([*]u8, ext.SDL_realloc(allocation, layout.length) + orelse return error.OutOfMemory); + } + + pub fn dealloc(_: *Self, allocation: [*]u8) void { + ext.SDL_free(allocation); + } + }, undefined)); } /// @@ -453,16 +645,11 @@ pub fn allocator() core.io.Allocator { pub fn display(comptime Error: anytype, comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void { - var cwd = FileSystem{.native = "./"}; + const cwd = FileSystem{.native = "./"}; const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure; defer ext.SDL_free(user_prefix); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - - defer if (gpa.deinit()) - ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION, "Runtime allocator leaked memory"); - var app = App{ .user = .{.native = std.mem.sliceTo(user_prefix, 0)}, @@ -480,17 +667,13 @@ pub fn display(comptime Error: anytype, return error.InitFailure; }, - .data = .{ - .archive_file = cwd.openRead(try Path.from(&.{"./data.oar"})) catch { - ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load ./data.oar"); - - return error.InitFailure; - }, - }, + .data = .{.archive = .{ + .file_system = &cwd, + .path = try Path.from(&.{"./data.oar"}), + }}, }; defer { - app.data.archive_file.close(); ext.SDL_DestroySemaphore(app.message_semaphore); ext.SDL_DestroyMutex(app.message_mutex); }