Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Support for Zig 0.17, alongside 0.16
- Decoding fixed-size arrays (`[N]T`), which could already be encoded
- Decoding single-item pointers (`*T`, `?*T`), including recursive types; a type holding a pointer to a struct or union decodes and encodes with `anyerror`

### Changed
- Encoding writes straight into the writer's buffer when there is room, and keeps the drain path out of line, which is about 13% cheaper on a typical struct and 37% on an array of integers
Expand Down
80 changes: 80 additions & 0 deletions src/any.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const Binary = @import("binary.zig").Binary;

const packArray = @import("array.zig").packArray;
const unpackArray = @import("array.zig").unpackArray;
const unpackFixedArray = @import("array.zig").unpackFixedArray;

const unpackNullIfPresent = @import("null.zig").unpackNullIfPresent;
const isOptional = @import("utils.zig").isOptional;

const packStruct = @import("struct.zig").packStruct;
const unpackStruct = @import("struct.zig").unpackStruct;
Expand Down Expand Up @@ -78,6 +82,7 @@ pub fn packAny(writer: *std.Io.Writer, value: anytype) !void {
},
}
} else if (ptr_info.size == .one) {
if (comptime isContainer(ptr_info.child)) return packPointee(writer, value);
return packAny(writer, value.*);
}
},
Expand Down Expand Up @@ -110,20 +115,55 @@ pub fn unpackAny(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime
.@"struct" => return unpackStruct(reader, allocator, T),
.@"union" => return unpackUnion(reader, allocator, T),
.@"enum" => return unpackEnum(reader, T),
.array => return unpackFixedArray(reader, allocator, T),
.pointer => |ptr_info| {
if (ptr_info.size == .slice) {
if (isString(T)) {
return unpackStringValue(reader, allocator, T);
} else {
return unpackArray(reader, allocator, T);
}
} else if (ptr_info.size == .one) {
if (comptime isContainer(ptr_info.child)) return unpackPointerToContainer(reader, allocator, T);
return unpackPointer(reader, allocator, T);
}
},
else => {},
}
@compileError("Unsupported type '" ++ @typeName(T) ++ "'");
}

/// A struct or union reached through a pointer may be recursive, holding a
/// pointer to its own type. Its inferred error set would then depend on itself,
/// which Zig rejects as a dependency loop, so the recursion through such a
/// pointer goes through a function with an explicit error set.
fn isContainer(comptime T: type) bool {
return switch (@typeInfo(T)) {
.@"struct", .@"union" => true,
else => false,
};
}

fn packPointee(writer: *std.Io.Writer, value: anytype) anyerror!void {
return packAny(writer, value.*);
}

fn unpackPointerToContainer(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime T: type) anyerror!T {
return unpackPointer(reader, allocator, T);
}

/// Allocates the pointee and unpacks into it. This is what recursive types
/// need, such as a node that holds an optional pointer to another node.
fn unpackPointer(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime T: type) !T {
if (isOptional(T) and try unpackNullIfPresent(reader)) return null;

const Child = @typeInfo(NonOptional(T)).pointer.child;
const ptr = try allocator.create(Child);
errdefer allocator.destroy(ptr);
ptr.* = try unpackAny(reader, allocator, Child);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return ptr;
}

test "packAny/unpackAny: bool" {
var buffer: [16]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
Expand Down Expand Up @@ -325,6 +365,46 @@ test "packAny/unpackAny: optional union" {
}
}

test "packAny/unpackAny: fixed array" {
var buffer: [16]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
try packAny(&writer, [2]u16{ 300, 7 });

var reader = std.Io.Reader.fixed(writer.buffered());
try std.testing.expectEqual([2]u16{ 300, 7 }, try unpackAny(&reader, std.testing.allocator, [2]u16));
}

test "packAny/unpackAny: pointer" {
var buffer: [16]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
const value: u32 = 1234;
try packAny(&writer, &value);

var reader = std.Io.Reader.fixed(writer.buffered());
const ptr = try unpackAny(&reader, std.testing.allocator, *const u32);
defer std.testing.allocator.destroy(ptr);
try std.testing.expectEqual(1234, ptr.*);
}

test "packAny/unpackAny: recursive struct through an optional pointer" {
const Node = struct {
value: u32,
next: ?*@This() = null,
};
var buffer: [32]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var tail: Node = .{ .value = 2 };
try packAny(&writer, Node{ .value = 1, .next = &tail });

var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var reader = std.Io.Reader.fixed(writer.buffered());
const head = try unpackAny(&reader, arena.allocator(), Node);
try std.testing.expectEqual(1, head.value);
try std.testing.expectEqual(2, head.next.?.value);
try std.testing.expectEqual(null, head.next.?.next);
}

test "packAny/unpackAny: String struct" {
var buffer: [64]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
Expand Down
54 changes: 54 additions & 0 deletions src/array.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const maybePackNull = @import("null.zig").maybePackNull;
const maybeUnpackNull = @import("null.zig").maybeUnpackNull;

const packHeaderAndInt = @import("utils.zig").packHeaderAndInt;
const readSliceFast = @import("utils.zig").readSliceFast;
const unpackStringHeader = @import("string.zig").unpackStringHeader;
const unpackIntValue = @import("int.zig").unpackIntValue;

const packAny = @import("any.zig").packAny;
Expand Down Expand Up @@ -83,6 +85,29 @@ pub fn unpackArray(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptim
return data;
}

/// Unpacks a fixed-size array, the counterpart of how `packAny` writes one: an
/// array of exactly `N` items, or for `[N]u8` a string of exactly `N` bytes.
pub fn unpackFixedArray(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime T: type) !T {
const info = @typeInfo(NonOptional(T)).array;
const unpackHeader = if (info.child == u8) unpackStringHeader else unpackArrayHeader;

const len = if (isOptional(T))
try unpackHeader(reader, ?u32) orelse return null
else
try unpackHeader(reader, u32);
if (len != info.len) return error.InvalidFormat;

var result: NonOptional(T) = undefined;
if (info.child == u8) {
try readSliceFast(reader, &result);
} else {
for (&result) |*item| {
item.* = try unpackAny(reader, allocator, info.child);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
return result;
}

pub fn unpackArrayInto(reader: *std.Io.Reader, allocator: std.mem.Allocator, comptime Item: type, buffer: []Item) ![]Item {
const len = try unpackArrayHeader(reader, u32);

Expand Down Expand Up @@ -131,6 +156,35 @@ test "packArray: null" {
try std.testing.expectEqualSlices(u8, &packed_null, writer.buffered());
}

test "unpackFixedArray: round trip" {
var buffer: [32]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
try packAny(&writer, [2]f64{ 1.5, -2.25 });

var reader = std.Io.Reader.fixed(writer.buffered());
try std.testing.expectEqual([2]f64{ 1.5, -2.25 }, try unpackFixedArray(&reader, std.testing.allocator, [2]f64));
}

test "unpackFixedArray: [N]u8 is a string, like packAny writes it" {
var buffer: [16]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
try packAny(&writer, [3]u8{ 'a', 'b', 'c' });

var reader = std.Io.Reader.fixed(writer.buffered());
try std.testing.expectEqual([3]u8{ 'a', 'b', 'c' }, try unpackFixedArray(&reader, std.testing.allocator, [3]u8));
}

test "unpackFixedArray: null" {
var reader = std.Io.Reader.fixed(&packed_null);
try std.testing.expectEqual(null, try unpackFixedArray(&reader, std.testing.allocator, ?[3]u32));
}

test "unpackFixedArray: wrong length" {
const packed_two = [_]u8{ 0x92, 0x01, 0x02 };
var reader = std.Io.Reader.fixed(&packed_two);
try std.testing.expectError(error.InvalidFormat, unpackFixedArray(&reader, std.testing.allocator, [3]u32));
}

test "sizeOfPackedArrayHeader: array16 boundary" {
try std.testing.expectEqual(3, sizeOfPackedArrayHeader(16));
}
Expand Down
Loading