Skip to content

Instantly share code, notes, and snippets.

@jjrv
Last active February 15, 2025 10:55
Show Gist options
  • Save jjrv/cb373bcbde3f5d6287977f5ad0c510f8 to your computer and use it in GitHub Desktop.
Save jjrv/cb373bcbde3f5d6287977f5ad0c510f8 to your computer and use it in GitHub Desktop.
const std = @import("std");
pub const Sizes = @Vector(4, u6);
const debug = true;
pub const BitReader = struct {
word_buf: [*]const u32,
bit_count: u6,
bits: u64,
// For debugging, bit offset within stream.
pos: usize = 0,
pub fn init(data: []const u8, pos: usize) BitReader {
const offset: u6 = @intCast(@intFromPtr(&data[pos / 32]) & 3);
const word_buf: [*]const u32 = @ptrCast(@alignCast(@as([*]const u8, @ptrCast(data[pos / 32 ..])) - offset));
const pos5: u5 = @truncate(pos);
const bit_count = 32 - offset * 8 - pos5;
return .{ //
// Without initial refill:
// .word_buf = word_buf + 1,
// .bit_count = bit_count,
// .bits = word_buf[0] >> pos5,
// With initial refill:
.word_buf = word_buf + ((bit_count >> 5) ^ 1) + 1,
.bit_count = bit_count | 32,
.bits = (word_buf[0] >> pos5) | (@as(u64, word_buf[1]) << bit_count),
.pos = pos,
};
}
// General bit stream operations
// -----------------------------
/// Fill buffer so at least 32 bits are available.
/// No bounds checking, caller must avoid reading past end of input.
pub inline fn refill(self: *BitReader) void {
self.bits |= @as(u64, self.word_buf[0]) << self.bit_count;
self.word_buf += (self.bit_count >> 5) ^ 1;
self.bit_count |= 32;
}
/// Read bits from buffer without consuming them.
/// You can also peek any integer type u1-u32 by directly accessing @truncate(bit_reader.bits).
pub inline fn peek(self: *BitReader, count: u6) u32 {
std.debug.assert(count <= 32);
std.debug.assert(self.bit_count >= count);
return @intCast(self.bits & ((@as(u64, 1) << count) - 1));
}
/// Discard 0-32 bits of input.
pub inline fn consume(self: *BitReader, count: u6) void {
std.debug.assert(count <= 32);
std.debug.assert(self.bit_count >= count);
self.bits >>= count;
self.bit_count -= count;
if (debug) self.pos += count;
}
/// Discard 0-32 bits of input and fill buffer so at least 32 bits are available.
/// No bounds checking, caller must avoid reading past end of input.
pub inline fn consumeRefill(self: *BitReader, count: u6) void {
std.debug.assert(count <= 32);
std.debug.assert(self.bit_count >= count);
const remain_count = self.bit_count - count;
self.bits = (self.bits >> count) | (@as(u64, self.word_buf[0]) << remain_count);
self.word_buf += (remain_count >> 5) ^ 1;
self.bit_count = remain_count | 32;
if (debug) self.pos += count;
}
/// Return given type of value (u1-u32 or equivalent) filled with input bits,
/// consume them from input buffer and re-fill it.
/// No bounds checking, caller must avoid reading past end of input.
pub fn get(self: *BitReader, Type: type) Type {
const info = @typeInfo(Type);
// If type is an enum or bool, use size of the represented integer.
const Int = switch (info) {
.int => Type,
.bool => u1,
.@"enum" => info.@"enum".tag_type,
else => @compileError("Unsupported type"),
};
const count = @typeInfo(Int).int.bits;
if (count > 32) @compileError("Type has too many bits");
defer self.consumeRefill(count);
const value: Int = @truncate(self.bits);
return switch (info) {
.int => value,
.bool => @bitCast(value),
.@"enum" => @enumFromInt(value),
else => @compileError("Unsupported type"),
};
}
/// Return given number of input bits (0-32), consume them from input buffer and re-fill it.
/// No bounds checking, caller must avoid reading past end of input.
pub fn get32(self: *BitReader, count: u6) u32 {
defer self.consumeRefill(count);
return self.peek(count);
}
// JPEG XL -specific operations
// ----------------------------
fn distribute(self: *BitReader, sizes: Sizes, comptime max_size: u6, Offset: type, offsets: @Vector(4, Offset)) u32 {
const distribution: u2 = @truncate(self.bits);
const prefix = if (max_size > 30) 0 else 2;
const count = (sizes + @as(Sizes, @splat(prefix)))[distribution];
if (max_size > 30) self.consumeRefill(2);
defer self.consumeRefill(count);
return (self.peek(count) >> prefix) + if (Offset == u1) 0 else offsets[distribution];
}
/// B.2.2 Read variable-length encoded u32.
pub inline fn getVar32(self: *BitReader, comptime sizes: Sizes, comptime offsets: ?@Vector(4, u32)) u32 {
const max_size = @reduce(.Max, sizes);
if (offsets) |off| {
const max_offset = @reduce(.Max, off);
const Offset = if (max_offset >= 0x10000) u32 else if (max_offset >= 0x100) u16 else u8;
return self.distribute(sizes, max_size, Offset, @Vector(4, Offset){ off[0], off[1], off[2], off[3] });
} else {
return self.distribute(sizes, max_size, u1, @splat(0));
}
}
/// B.2.3 Read variable-length encoded u64 (here only up to u32).
pub fn getVar64(self: *BitReader) u32 {
const distribution: u2 = @truncate(self.bits);
return if (distribution == 3) block: {
var result: u64 = self.get(u12);
var shift: u6 = 12;
while (self.get(bool)) {
if (shift == 60) {
result |= @as(u64, self.get(u4)) << 60;
break;
}
result |= @as(u64, self.get(u8)) << shift;
shift += 8;
}
std.debug.assert(result <= 0xffffffff);
break :block @intCast(result);
} else self.getVar32(.{ 0, 4, 8, 0 }, .{ 0, 1, 17, 0 });
}
/// B.2.4 Read 16-bit float.
pub fn getF16(self: *BitReader) f32 {
const bits: u32 = self.get(u16);
const sign = (bits & 0x8000) << 16;
const exp_mantissa = ((bits & 0x7c00) + 0x1c000 + (bits & 0x3ff)) << 13;
return @bitCast(sign | exp_mantissa);
}
/// B.2.6 Read enum.
pub fn getEnum(self: *BitReader) u6 {
return @intCast(self.getVar32(.{ 0, 0, 4, 6 }, .{ 0, 1, 2, 18 }));
}
/// B.2.7 Skip to byte boundary.
pub fn skipToByte(self: *BitReader) void {
const count = self.bit_count & 7;
std.debug.assert(self.peek(count) == 0);
self.consume(count);
}
};
test {
var rng = std.Random.DefaultPrng.init(1);
const expect = std.testing.expect;
const rand = rng.random();
var buf: [256]u8 = undefined;
for (0..10) |_| {
for (0..buf.len) |i| {
buf[i] = rand.uintLessThan(u8, 255);
}
inline for (1..33) |size| {
comptime var info = @typeInfo(u32);
info.int.bits = size;
const Type = @Type(info);
for (0..size) |offset| {
var pos: usize = offset;
var br1 = BitReader.init(buf[0..buf.len], pos);
var br2 = BitReader.init(buf[0..buf.len], pos);
while (pos + size < buf.len * 8) {
var correct: u64 = 0;
for (0..size) |i| {
correct |= @as(u64, (buf[(pos + i) / 8] >> @truncate(pos + i)) & 1) << @truncate(i);
}
try expect(br1.get32(@truncate(size)) == correct);
if (size == 1) {
try expect(br2.get(bool) == (correct != 0));
} else {
try expect(br2.get(Type) == correct);
}
pos += size;
}
}
}
}
}
const std = @import("std");
const BitReader = @import("BitReader.zig").BitReader;
const R = @import("Reader.zig");
const tables = @import("tables.zig");
const BundleReader = R.BundleReader;
const some = R.some;
const neq = R.neq;
const If = R.If;
const Repeat = R.Repeat;
pub fn getName(br: *BitReader, allocator: std.mem.Allocator) !?[]u8 {
const name_len: u16 = @truncate(br.getVar32(.{ 0, 4, 5, 10 }, .{ 0, 0, 16, 48 }));
if (name_len == 0) return null;
var name = try allocator.alloc(u8, name_len);
for (0..name_len) |n| name[n] = br.get(u8);
return name;
}
/// B.3
pub const Extensions = struct { //
extensions: u32 = 0,
extension_bits: ?[]u32 = null,
pub fn init(br: *BitReader, allocator: std.mem.Allocator) !Extensions {
var r = BundleReader(Extensions, void).init(br, allocator, null);
errdefer r.target.deinit(allocator);
return try r.get(.{
.{ .extensions, .Var64 },
.{ Repeat(.extensions), .extension_bits, .Var64 },
});
}
pub fn deinit(self: *const Extensions, allocator: std.mem.Allocator) void {
if (self.extension_bits) |bits| {
allocator.free(bits);
}
}
};
/// D.1
pub const ImageHeader = struct { //
size: SizeHeader = .{},
metadata: ImageMetadata = .{},
pub fn init(br: *BitReader, allocator: std.mem.Allocator) !ImageHeader {
var r = BundleReader(ImageHeader, void).init(br, allocator, null);
return try r.get(.{
.{.size}, //
.{ .metadata, .{allocator} },
});
}
pub fn deinit(self: *const ImageHeader, allocator: std.mem.Allocator) void {
self.metadata.deinit(allocator);
}
/// D.2
pub const SizeHeader = struct { //
width: u32 = 0,
height: u32 = 0,
pub fn init(br: *BitReader) SizeHeader {
const div8 = br.get(bool);
const height = getDimension(br, div8);
const ratio = br.get(u3);
return .{ //
.width = if (ratio == 0) getDimension(br, div8) else applyRatio(height, ratio),
.height = height,
};
}
fn getDimension(br: *BitReader, div8: bool) u32 {
return if (div8)
(br.get32(5) + 1) << 3
else
br.getVar32(.{ 9, 13, 18, 30 }, null) + 1;
}
pub fn applyRatio(height: u32, ratio: u3) u32 {
return @truncate( //
@as(u64, height) *
([_]u8{ 0, 1, 6, 4, 3, 16, 5, 2 })[ratio] /
([_]u8{ 0, 1, 5, 3, 2, 9, 4, 1 })[ratio] //
);
}
};
/// D.3.1
pub const ImageMetadata = struct {
orientation: Orientation = .top_left,
intrinsic_size: ?SizeHeader = null,
preview: ?PreviewHeader = null,
animation: ?AnimationHeader = null,
bit_depth: ?BitDepth = null,
modular_16bit_buffers: bool = true,
num_extra: u16 = 0,
ec_info: ?[]ExtraChannelInfo = null,
xyb_encoded: bool = true,
colour_encoding: ?ColourEncoding = null,
tone_mapping: ?ToneMapping = null,
extensions: ?Extensions = null,
opsin_inverse_matrix: ?OpsinInverseMatrix = null,
up2_weight: [15]f32 = tables.up2_weight,
up4_weight: [55]f32 = tables.up4_weight,
up8_weight: [210]f32 = tables.up8_weight,
pub fn init(br: *BitReader, allocator: std.mem.Allocator) !ImageMetadata {
var flags: struct { //
all_default: bool = false,
extra_fields: bool = false,
have_intr_size: bool = false,
have_preview: bool = false,
have_animation: bool = false,
default_m: bool = false,
cw_mask: u3 = 0,
} = .{};
var r = BundleReader(ImageMetadata, @TypeOf(flags)).init(br, allocator, &flags);
errdefer r.target.deinit(allocator);
const custom_schema = .{
.{.extra_fields},
.{
If(.extra_fields, true), .{
.{.orientation}, //
.{.have_intr_size},
.{ If(.have_intr_size, true), .intrinsic_size },
.{.have_preview},
.{ If(.have_preview, true), .preview },
.{.have_animation},
.{ If(.have_animation, true), .animation },
},
},
.{.bit_depth}, //
.{.modular_16bit_buffers},
.{ .num_extra, .{ 0, 0, 4, 12 }, .{ 0, 1, 2, 1 } },
.{ Repeat(.num_extra), .ec_info, .{allocator} },
.{.xyb_encoded},
.{.colour_encoding},
.{ If(.extra_fields, true), .tone_mapping },
.{ .extensions, .{allocator} },
.{.default_m},
.{
If(.default_m, false), .{ //
.{ If(.xyb_encoded, true), .opsin_inverse_matrix },
.{.cw_mask},
.{ If(.cw_mask, some(u3, 1)), .up2_weight },
.{ If(.cw_mask, some(u3, 2)), .up4_weight },
.{ If(.cw_mask, some(u3, 4)), .up8_weight },
},
},
};
return try r.get(.{
.{.all_default},
.{ If(.all_default, false), custom_schema },
});
}
pub fn deinit(self: *const ImageMetadata, allocator: std.mem.Allocator) void {
if (self.ec_info) |ec_info| {
for (0..self.num_extra) |n| ec_info[n].deinit(allocator);
allocator.free(ec_info);
}
if (self.extensions) |extensions| extensions.deinit(allocator);
}
/// D.3.2
pub const Orientation = enum(u3) { //
top_left = 0,
top_right = 1,
bottom_right = 2,
bottom_left = 3,
left_top = 4,
right_top = 5,
right_bottom = 6,
left_bottom = 7,
};
/// D.3.3
pub const PreviewHeader = struct {
width: u32 = 8,
height: u32 = 8,
pub fn init(br: *BitReader) PreviewHeader {
const div8 = br.get(bool);
const height = getDimension(br, div8);
const ratio = br.get(u3);
return .{ //
.width = if (ratio == 0) getDimension(br, div8) else SizeHeader.applyRatio(height, ratio),
.height = height,
};
}
fn getDimension(br: *BitReader, div8: bool) u32 {
return if (div8)
br.getVar32(.{ 0, 0, 5, 9 }, .{ 16, 32, 1, 33 }) << 3
else
br.getVar32(.{ 6, 8, 10, 12 }, .{ 1, 65, 321, 1345 });
}
};
/// D.3.4
pub const AnimationHeader = struct { //
tps_numerator: u32 = 0,
tps_denominator: u16 = 0,
num_loops: u32 = 0,
have_timecodes: bool = false,
pub fn init(br: *BitReader) AnimationHeader {
var r = BundleReader(AnimationHeader, void).init(br, null, null);
return try r.get(.{
.{ .tps_numerator, .{ 0, 0, 10, 30 }, .{ 100, 1000, 1, 1 } },
.{ .tps_denominator, .{ 0, 0, 8, 10 }, .{ 1, 1001, 1, 1 } },
.{ .num_loops, .{ 0, 3, 16, 32 } },
.{.have_timecodes},
});
}
};
/// D.3.5
pub const BitDepth = struct { //
bits_per_sample: u8 = 8,
exp_bits: u8 = 0,
pub fn init(br: *BitReader) BitDepth {
var flags: struct { float_sample: bool = false } = .{};
var r = BundleReader(BitDepth, @TypeOf(flags)).init(br, null, &flags);
return try r.get(.{
.{.float_sample},
.{ If(.float_sample, true), .{
.{ .bits_per_sample, .{ 0, 0, 0, 6 }, .{ 32, 16, 24, 1 } },
.{ .exp_bits, 4, 1 },
} },
.{ If(.float_sample, false), .bits_per_sample, .{ 0, 0, 0, 6 }, .{ 8, 10, 12, 1 } },
});
}
};
/// D.3.6
pub const ExtraChannelInfo = struct { //
channel_type: ExtraChannelType = .alpha,
bit_depth: ?BitDepth = null,
dim_shift: u8 = 0,
name: ?[]const u8 = null,
alpha_associated: bool = false,
red: f32 = 0,
green: f32 = 0,
blue: f32 = 0,
solidity: f32 = 0,
cfa_channel: u16 = 1,
pub fn init(br: *BitReader, allocator: std.mem.Allocator) !ExtraChannelInfo {
var flags: struct { d_alpha: bool = false } = .{};
var r = BundleReader(ExtraChannelInfo, @TypeOf(flags)).init(br, null, &flags);
var info = try r.get(.{
.{.d_alpha},
.{ If(.d_alpha, false), .{
.{ .channel_type, .Enum },
.{.bit_depth},
.{ .dim_shift, .{ 0, 0, 0, 3 }, .{ 0, 3, 4, 1 } },
} },
});
// TODO move to schema on lines above.
if (!flags.d_alpha) {
info.name = try getName(br, allocator);
switch (info.channel_type) {
.alpha => info.alpha_associated = br.get(bool),
.spot_colour => {
info.red = br.getF16();
info.green = br.getF16();
info.blue = br.getF16();
info.solidity = br.getF16();
},
.cfa => {
info.cfa_channel = @truncate(br.getVar32(.{ 0, 2, 4, 8 }, .{ 1, 0, 3, 19 }));
},
else => {},
}
}
return info;
}
pub fn deinit(self: *const ExtraChannelInfo, allocator: std.mem.Allocator) void {
if (self.name) |name| allocator.free(name);
}
pub const ExtraChannelType = enum(u6) { //
alpha = 0,
depth = 1,
spot_colour = 2,
selection_mask = 3,
black = 4,
cfa = 5,
thermal = 6,
non_optional = 15,
optional = 16,
_,
};
};
/// E.2 / Table E.1
pub const ColourEncoding = struct { //
want_icc: bool = false,
colour_space: ColourSpace = .rgb,
white_point: WhitePoint = .d65,
white: ?CustomXY = null,
primaries: Primaries = .srgb,
red: ?CustomXY = null,
green: ?CustomXY = null,
blue: ?CustomXY = null,
tf: ?CustomTransferFunction = null,
rendering_intent: RenderingIntent = .relative,
pub fn init(br: *BitReader) ColourEncoding {
var flags: struct { all_default: bool = false } = .{};
var r = BundleReader(ColourEncoding, @TypeOf(flags)).init(br, null, &flags);
const custom_schema = .{
.{.want_icc},
.{ .colour_space, .Enum },
.{
If(.want_icc, false), .{
.{
If(.colour_space, neq(ColourSpace, .xyb)),
.{
.{ .white_point, .Enum },
.{ If(.white_point, WhitePoint.custom), .white },
.{ If(.colour_space, neq(ColourSpace, .grey)), .{
.{ .primaries, .Enum },
.{ If(.primaries, Primaries.custom), .{
.{.red},
.{.green},
.{.blue},
} },
} },
// SPEC missing not_xyb
.{.tf},
},
},
.{ .rendering_intent, .Enum },
},
},
};
return try r.get(.{
.{.all_default}, //
.{ If(.all_default, false), custom_schema },
});
}
/// Table E.2
pub const CustomXY = struct {
// TODO: Implement signed unpack, then change these to i32.
ux: u32 = 0,
uy: u32 = 0,
pub fn init(br: *BitReader) CustomXY {
var r = BundleReader(CustomXY, void).init(br, null, null);
return try r.get(.{ .{.ux}, .{.uy} });
}
};
/// Table E.3
pub const ColourSpace = enum(u6) { //
rgb = 0,
grey = 1,
xyb = 2,
unknown = 3,
_,
};
/// Table E.4
pub const WhitePoint = enum(u6) { //
d65 = 1,
custom = 2,
e = 10,
dci = 11,
_,
};
/// Table E.5
pub const Primaries = enum(u6) { //
srgb = 1,
custom = 2,
bt2100 = 9,
p3 = 11,
_,
};
/// Table E.7
pub const CustomTransferFunction = struct {
gamma: ?u32 = null,
transfer_function: TransferFunction = .srgb,
pub fn init(br: *BitReader) CustomTransferFunction {
var flags: struct { have_gamma: bool = false } = .{};
var r = BundleReader(CustomTransferFunction, @TypeOf(flags)).init(br, null, &flags);
return try r.get(.{
.{.have_gamma},
.{ If(.have_gamma, true), .gamma, 24 },
.{ If(.have_gamma, false), .transfer_function, .Enum },
});
}
/// Table E.6
pub const TransferFunction = enum(u6) { //
bt709 = 1,
unknown = 2,
linear = 8,
srgb = 13,
pq = 16,
dci = 17,
hlg = 18,
};
};
/// Table E.8
pub const RenderingIntent = enum(u6) { //
perceptual = 0,
relative = 1,
saturation = 2,
absolute = 3,
_,
};
};
/// E.3 / Table E.9
pub const ToneMapping = struct { //
intensity_target: f32 = 255,
min_nits: f32 = 0,
relative_to_max_display: bool = false,
linear_below: f32 = 0,
pub fn init(br: *BitReader) ToneMapping {
var flags: struct { all_default: bool = false } = .{};
var r = BundleReader(ToneMapping, @TypeOf(flags)).init(br, null, &flags);
return try r.get(.{
.{.all_default},
.{
If(.all_default, false), .{ //
.{.intensity_target},
.{.min_nits},
.{.relative_to_max_display},
.{.linear_below},
},
},
});
}
};
pub const OpsinInverseMatrix = struct { //
inv_mat: [9]f32 = [_]f32{ //
11.031566901960783, -9.866943921568629, -0.16462299647058826,
-3.254147380392157, 4.418770392156863, -0.16462299647058826,
-3.6588512862745097, 2.7129230470588235, 1.9459282392156863,
},
opsin_bias: [3]f32 = [_]f32{ opsin_bias_term, opsin_bias_term, opsin_bias_term },
quant_bias: [3]f32 = [_]f32{ //
1 - 0.05465007330715401,
1 - 0.07005449891748593,
1 - 0.049935103337343655,
},
quant_bias_numerator: f32 = 0.145,
const opsin_bias_term = 0.0037930732552754493;
pub fn init(br: *BitReader) OpsinInverseMatrix {
var flags: struct { all_default: bool = false } = .{};
var r = BundleReader(OpsinInverseMatrix, @TypeOf(flags)).init(br, null, &flags);
return try r.get(.{
.{.all_default},
.{
If(.all_default, false), .{ //
.{.inv_mat},
.{.opsin_bias},
.{.quant_bias},
.{.quant_bias_numerator},
},
},
});
}
};
};
};
const std = @import("std");
const BitReader = @import("BitReader.zig").BitReader;
const FlowKind = enum(u8) { If, Repeat };
pub fn If(flag: anytype, equals: anytype) type {
return struct {
const kind: FlowKind = .If;
const name = @tagName(flag);
const value = equals;
};
}
pub fn Repeat(flag: anytype) type {
return struct {
const kind: FlowKind = .Repeat;
const name = if (@typeInfo(@TypeOf(flag)) == .enum_literal) @tagName(flag) else @compileError("Repeat(flag): flag must be an enum literal");
};
}
/// Create an "iterator" for reading structs from a JPEG XL bit stream based on schemas,
/// storing values for different fields in target or scratch (for condition flags) structs.
pub fn BundleReader(Type: type, Scratch: type) type {
return struct {
br: *BitReader,
allocator: ?std.mem.Allocator = null,
target: Type = .{},
scratch: ?*Scratch = null,
pub fn init(br: *BitReader, allocator: ?std.mem.Allocator, scratch: ?*Scratch) @This() {
return .{ .br = br, .allocator = allocator, .scratch = scratch };
}
// Parse a JPEG XL bundle according to given schema.
pub fn get(self: *@This(), schema: anytype) !Type {
inline for (schema) |entry| {
comptime var arg = 0;
comptime var entry_type = @typeInfo(@TypeOf(entry[arg]));
var repeat: u32 = 1;
// Entry starting with a type represents flow control.
if (entry_type == .type) {
const target = if (@hasField(Type, entry[arg].name)) &self.target else self.scratch.?;
repeat = switch (entry[arg].kind) {
.If => block: {
if (@typeInfo(@TypeOf(entry[arg].value)) == .@"fn") {
// If condition is a function, call it passing the parsed struct so far.
// Only parse the entry if it returns true.
break :block if (@call(.always_inline, entry[arg].value, .{@field(target, entry[arg].name)})) 1 else 0;
} else {
// Otherwise condition should be the name of a boolean field
// in target or scratch struct. Only parse the entry if it's true.
break :block if (@field(target, entry[arg].name) == entry[arg].value) 1 else 0;
}
},
// Repeat defines how many times entry should be stored in target slice.
// Don't use if target is a fixed size array (repeat count is known).
.Repeat => @field(target, entry[arg].name),
};
// Consume flow control directive and parse rest of entry.
arg += 1;
entry_type = @typeInfo(@TypeOf(entry[arg]));
}
// Entry starting with a struct represents a list of entries,
// grouped into a block for flow control.
if (entry_type == .@"struct") {
if (repeat == 1) _ = try self.get(entry[arg]);
continue;
}
// Entry starting with an enum tag represents storing a value to target or scratch struct.
const key_name = @tagName(entry[arg]);
const is_pub = @hasField(Type, key_name);
if (!is_pub and !@hasField(Scratch, key_name)) @compileError("Unknown field name");
// If enum tag matches a field name in target struct, store value there, otherwise matching field in scratch struct.
const target = if (is_pub) &self.target else self.scratch.?;
arg += 1;
// Get type of field in target or scratch struct.
comptime var FieldType = @FieldType(if (is_pub) Type else Scratch, key_name);
comptime var field_info = @typeInfo(FieldType);
// Type of field possibly still wrapped in an optional.
const Wrapped = FieldType;
if (field_info == .optional) {
// Unwrap optional type.
FieldType = field_info.optional.child;
field_info = @typeInfo(FieldType);
}
const is_slice = field_info == .pointer and field_info.pointer.size == .slice;
var dst = switch (field_info) {
.array => block: {
// If target is an array, repeat entry to fill it.
FieldType = field_info.array.child;
field_info = @typeInfo(FieldType);
repeat = @field(target, key_name).len;
break :block @as([*]FieldType, @ptrCast(&@field(target, key_name)[0]));
},
.pointer => block: {
// If target is a slice, allocate memory to fit given number of repeated entries.
FieldType = field_info.pointer.child;
field_info = @typeInfo(FieldType);
const slice = try self.allocator.?.alloc(FieldType, repeat);
@field(target, key_name) = slice;
break :block @as([*]FieldType, @ptrCast(slice));
},
// Otherwise, store value directly in the field. If the field is optional,
// the pointer type we write to must reflect that for the field to be readable.
else => @as([*]Wrapped, @ptrCast(&@field(target, key_name))),
};
errdefer if (is_slice) {
// On error deallocate memory allocated for this entry.
// Any previous entries have already been written to the target or scratch struct,
// so the caller can deinit them.
if (field_info == .@"struct" and @hasDecl(FieldType, "deinit")) {
const slice = @field(target, key_name).?;
const deinit_args = @typeInfo(@TypeOf(FieldType.deinit)).@"fn".params;
// Deinit already inited items before deallocating slice.
for (0..dst - @as([*]FieldType, @ptrCast(slice))) |n| {
if (deinit_args.len == 2) slice[n].deinit(self.allocator.?) else slice[n].deinit();
}
}
self.allocator.?.free(@field(target, key_name).?);
// Clear field in target struct since now it points to deallocated memory.
@field(target, key_name) = null;
};
// Print field name and bit offset. TODO: Comment this out...
std.debug.print("{s}: {}\n", .{ key_name, self.br.pos });
while (repeat > 0) {
const value = if (field_info == .@"struct" and @hasDecl(FieldType, "init")) block: {
// If field is a struct with an init function, call it.
const init_fn = FieldType.init;
const init_info = @typeInfo(@TypeOf(init_fn)).@"fn";
// Always pass bit reader as the first arg, then any other args given.
const args = .{self.br} ++ (if (entry.len > arg) entry[arg] else .{});
// Call init handling possible error.
break :block if (@typeInfo(init_info.return_type.?) == .error_union) try @call(.auto, init_fn, args) else @call(.auto, init_fn, args);
} else if (entry.len <= arg)
// If field is a bool or integer and no other args given, fill it using bit reader.
// Read floats encoded using 16 bits.
if (FieldType == f32) self.br.getF16() else self.br.get(FieldType)
else switch (@typeInfo(@TypeOf(entry[arg]))) {
// If first given arg is a number, treat it as number of bits to read.
// Second arg, if present, is added to the value that was read.
.comptime_int => self.br.get32(entry[arg]) + if (entry.len > arg + 1) entry[arg + 1] else 0,
// If first given arg is a literal, it signals some special field types.
.enum_literal => switch (entry[arg]) {
// Enums have special encoding supporting values 0-81.
.Enum => self.br.getEnum(),
// Variable-length 64-bit encoding.
.Var64 => self.br.getVar64(),
else => @compileError("Unsupported entry argument type"),
},
// If first given arg is a struct, treat it as a list of 4 possible bit counts to read,
// after reading a 2-bit "distribution" selector. Second arg, if present,
// is a list of 4 corresponding numbers to add to the value that was read.
.@"struct" => self.br.getVar32(entry[arg], if (entry.len > arg + 1) entry[arg + 1] else null),
else => @compileError("Unsupported entry argument type"),
};
switch (field_info) {
.@"enum" => dst[0] = if (@typeInfo(@TypeOf(value)) == .@"enum") value else @enumFromInt(value),
// TODO: Unpack signed as needed
.int => dst[0] = @intCast(value),
// TODO: Bitcast if packed struct backed by integer.
else => dst[0] = value,
}
dst += 1;
repeat -= 1;
}
}
return self.target;
}
};
}
pub fn some(Type: type, mask: Type) fn (Type) bool {
return struct {
pub fn check(value: Type) bool {
return value & mask != 0;
}
}.check;
}
pub fn neq(Type: type, not_equals: Type) fn (Type) bool {
return struct {
pub fn check(value: Type) bool {
return value != not_equals;
}
}.check;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment