├── .gitignore ├── LICENSE ├── README.md ├── borsh.zig └── build.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kenta Iwasaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # borsh-zig 2 | 3 | A [Zig](https://ziglang.org) implementation of the [Borsh](https://borsh.io/) binary format specification. 4 | 5 | ## Specification 6 | 7 | Refer to the official [specification](https://borsh.io/). -------------------------------------------------------------------------------- /borsh.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const testing = std.testing; 4 | 5 | const borsh = @This(); 6 | 7 | /// An optional type whose enum tag is 32 bits wide. 8 | pub fn Option(comptime T: type) type { 9 | return union(enum(u32)) { 10 | none: void, 11 | some: T, 12 | 13 | pub fn from(inner: ?T) @This() { 14 | if (inner) |payload| { 15 | return .{ .some = payload }; 16 | } 17 | return .none; 18 | } 19 | 20 | pub fn into(self: @This()) ?T { 21 | return switch (self) { 22 | .some => |payload| payload, 23 | .none => null, 24 | }; 25 | } 26 | 27 | pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 28 | return switch (self) { 29 | .none => writer.writeAll("null"), 30 | .some => |payload| writer.print("{any}", .{payload}), 31 | }; 32 | } 33 | }; 34 | } 35 | 36 | pub fn sizeOf(data: anytype) usize { 37 | var stream = std.io.countingWriter(std.io.null_writer); 38 | borsh.write(stream.writer(), data) catch unreachable; 39 | return @as(usize, @intCast(stream.bytes_written)); 40 | } 41 | 42 | pub fn readFromSlice(gpa: std.mem.Allocator, comptime T: type, slice: []const u8) !T { 43 | var stream = std.io.fixedBufferStream(slice); 44 | return borsh.read(gpa, T, stream.reader()); 45 | } 46 | 47 | pub fn writeToSlice(slice: []u8, data: anytype) ![]u8 { 48 | var stream = std.io.fixedBufferStream(slice); 49 | try borsh.write(stream.writer(), data); 50 | return stream.getWritten(); 51 | } 52 | 53 | pub inline fn writeAlloc(gpa: std.mem.Allocator, data: anytype) ![]u8 { 54 | const buffer = try gpa.alloc(u8, borsh.sizeOf(data)); 55 | errdefer gpa.free(buffer); 56 | return try borsh.writeToSlice(buffer, data); 57 | } 58 | 59 | pub fn read(gpa: std.mem.Allocator, comptime T: type, reader: anytype) !T { 60 | switch (@typeInfo(T)) { 61 | .Void => return {}, 62 | .Bool => return switch (try reader.readByte()) { 63 | 0 => false, 64 | 1 => true, 65 | else => error.BadBoolean, 66 | }, 67 | .Enum => |info| { 68 | const tag = try borsh.read(gpa, u8, reader); 69 | return std.meta.intToEnum(T, @as(info.tag_type, @intCast(tag))); 70 | }, 71 | .Union => |info| { 72 | const tag_type = info.tag_type orelse @compileError("Only tagged unions may be read."); 73 | const raw_tag = try borsh.read(gpa, tag_type, reader); 74 | 75 | inline for (info.fields) |field| { 76 | if (raw_tag == @field(tag_type, field.name)) { 77 | // https://github.com/ziglang/zig/issues/7866 78 | if (field.type == void) return @unionInit(T, field.name, {}); 79 | const payload = try borsh.read(gpa, field.type, reader); 80 | return @unionInit(T, field.name, payload); 81 | } 82 | } 83 | 84 | return error.UnknownUnionTag; 85 | }, 86 | .Struct => |info| { 87 | var data: T = undefined; 88 | inline for (info.fields) |field| { 89 | if (!field.is_comptime) { 90 | @field(data, field.name) = try borsh.read(gpa, field.type, reader); 91 | } 92 | } 93 | return data; 94 | }, 95 | .Optional => |info| { 96 | return switch (try reader.readByte()) { 97 | 0 => null, 98 | 1 => try borsh.read(gpa, info.child, reader), 99 | else => error.BadOptionalBoolean, 100 | }; 101 | }, 102 | .Array => |info| { 103 | var data: T = undefined; 104 | for (&data) |*element| { 105 | element.* = try borsh.read(gpa, info.child, reader); 106 | } 107 | return data; 108 | }, 109 | .Vector => |info| { 110 | var data: T = undefined; 111 | for (&data) |*element| { 112 | element.* = try borsh.read(gpa, info.child, reader); 113 | } 114 | return data; 115 | }, 116 | .Pointer => |info| { 117 | switch (info.size) { 118 | .One => { 119 | const data = try gpa.create(info.child); 120 | errdefer gpa.destroy(data); 121 | data.* = try borsh.read(gpa, info.child, reader); 122 | return data; 123 | }, 124 | .Slice => { 125 | const entries = try gpa.alloc(info.child, try borsh.read(gpa, u32, reader)); 126 | errdefer gpa.free(entries); 127 | for (entries) |*entry| { 128 | entry.* = try borsh.read(gpa, info.child, reader); 129 | } 130 | return entries; 131 | }, 132 | else => {}, 133 | } 134 | }, 135 | .ComptimeFloat => return borsh.read(gpa, f64, reader), 136 | .Float => |info| { 137 | const data = @as(T, @bitCast(try reader.readBytesNoEof((info.bits + 7) / 8))); 138 | if (std.math.isNan(data)) { 139 | return error.FloatIsNan; 140 | } 141 | return data; 142 | }, 143 | .ComptimeInt => return borsh.read(gpa, u64, reader), 144 | .Int => return reader.readIntLittle(T), 145 | else => {}, 146 | } 147 | 148 | @compileError("Deserializing '" ++ @typeName(T) ++ "' is unsupported."); 149 | } 150 | 151 | pub fn readFree(gpa: std.mem.Allocator, value: anytype) void { 152 | const T = @TypeOf(value); 153 | switch (@typeInfo(T)) { 154 | .Array, .Vector => { 155 | for (value) |element| { 156 | borsh.readFree(gpa, element); 157 | } 158 | }, 159 | .Struct => |info| { 160 | inline for (info.fields) |field| { 161 | if (!field.is_comptime) { 162 | borsh.readFree(gpa, @field(value, field.name)); 163 | } 164 | } 165 | }, 166 | .Optional => { 167 | if (value) |v| { 168 | borsh.readFree(gpa, v); 169 | } 170 | }, 171 | .Union => |info| { 172 | inline for (info.fields) |field| { 173 | if (value == @field(T, field.name)) { 174 | return borsh.readFree(gpa, @field(value, field.name)); 175 | } 176 | } 177 | }, 178 | .Pointer => |info| { 179 | switch (info.size) { 180 | .One => gpa.destroy(value), 181 | .Slice => { 182 | for (value) |item| { 183 | borsh.readFree(gpa, item); 184 | } 185 | gpa.free(value); 186 | }, 187 | else => {}, 188 | } 189 | }, 190 | else => {}, 191 | } 192 | } 193 | 194 | pub fn write(writer: anytype, data: anytype) !void { 195 | const T = @TypeOf(data); 196 | switch (@typeInfo(T)) { 197 | .Type, .Void, .NoReturn, .Undefined, .Null, .Fn, .Opaque, .Frame, .AnyFrame => return, 198 | .Bool => return writer.writeByte(@intFromBool(data)), 199 | .Enum => return borsh.write(writer, std.math.cast(u8, @intFromEnum(data)) orelse return error.EnumTooLarge), 200 | .Union => |info| { 201 | try borsh.write(writer, std.math.cast(u8, @intFromEnum(data)) orelse return error.EnumTooLarge); 202 | inline for (info.fields) |field| { 203 | if (data == @field(T, field.name)) { 204 | return borsh.write(writer, @field(data, field.name)); 205 | } 206 | } 207 | return; 208 | }, 209 | .Struct => |info| { 210 | var maybe_err: anyerror!void = {}; 211 | inline for (info.fields) |field| { 212 | if (!field.is_comptime) { 213 | if (@as(?anyerror!void, maybe_err catch null) != null) { 214 | maybe_err = borsh.write(writer, @field(data, field.name)); 215 | } 216 | } 217 | } 218 | return maybe_err; 219 | }, 220 | .Optional => { 221 | if (data) |value| { 222 | try writer.writeByte(1); 223 | try borsh.write(writer, value); 224 | } else { 225 | try writer.writeByte(0); 226 | } 227 | return; 228 | }, 229 | .Array, .Vector => { 230 | for (data) |element| { 231 | try borsh.write(writer, element); 232 | } 233 | return; 234 | }, 235 | .Pointer => |info| { 236 | switch (info.size) { 237 | .One => return borsh.write(writer, data.*), 238 | .Many => return borsh.write(writer, std.mem.span(data)), 239 | .Slice => { 240 | try borsh.write(writer, std.math.cast(u32, data.len) orelse return error.DataTooLarge); 241 | for (data) |element| { 242 | try borsh.write(writer, element); 243 | } 244 | return; 245 | }, 246 | else => {}, 247 | } 248 | }, 249 | .ComptimeFloat => return borsh.write(writer, @as(f64, data)), 250 | .Float => { 251 | if (std.math.isNan(data)) { 252 | return error.FloatsMayNotBeNan; 253 | } 254 | return writer.writeAll(std.mem.asBytes(&data)); 255 | }, 256 | .ComptimeInt => { 257 | if (data < 0) { 258 | @compileError("Signed comptime integers can not be serialized."); 259 | } 260 | return borsh.write(writer, @as(u64, data)); 261 | }, 262 | .Int => return writer.writeIntLittle(T, data), 263 | else => {}, 264 | } 265 | 266 | @compileError("Serializing '" ++ @typeName(T) ++ "' is unsupported."); 267 | } 268 | 269 | test "borsh: serialize and deserialize" { 270 | var buffer = std.ArrayList(u8).init(testing.allocator); 271 | defer buffer.deinit(); 272 | 273 | inline for (.{ 274 | @as(i8, std.math.minInt(i8)), 275 | @as(i16, std.math.minInt(i16)), 276 | @as(i32, std.math.minInt(i32)), 277 | @as(i64, std.math.minInt(i64)), 278 | @as(i8, std.math.maxInt(i8)), 279 | @as(i16, std.math.maxInt(i16)), 280 | @as(i32, std.math.maxInt(i32)), 281 | @as(i64, std.math.maxInt(i64)), 282 | @as(u8, std.math.maxInt(u8)), 283 | @as(u16, std.math.maxInt(u16)), 284 | @as(u32, std.math.maxInt(u32)), 285 | @as(u64, std.math.maxInt(u64)), 286 | 287 | @as(f32, std.math.floatMin(f32)), 288 | @as(f64, std.math.floatMin(f64)), 289 | @as(f32, std.math.floatMax(f32)), 290 | @as(f64, std.math.floatMax(f64)), 291 | 292 | [_]u8{ 0, 1, 2, 3 }, 293 | }) |expected| { 294 | try borsh.write(buffer.writer(), expected); 295 | var stream = std.io.fixedBufferStream(buffer.items); 296 | 297 | const actual = try borsh.read(testing.allocator, @TypeOf(expected), stream.reader()); 298 | defer borsh.readFree(testing.allocator, actual); 299 | 300 | try testing.expectEqual(expected, actual); 301 | buffer.clearRetainingCapacity(); 302 | } 303 | 304 | inline for (.{ 305 | "hello world", 306 | @as([]const u8, "hello world"), 307 | }) |expected| { 308 | try borsh.write(buffer.writer(), expected); 309 | var stream = std.io.fixedBufferStream(buffer.items); 310 | 311 | const actual = try borsh.read(testing.allocator, @TypeOf(expected), stream.reader()); 312 | defer borsh.readFree(testing.allocator, actual); 313 | 314 | try testing.expectEqualSlices(std.meta.Elem(@TypeOf(expected)), expected, actual); 315 | buffer.clearRetainingCapacity(); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const module = b.createModule(.{ 8 | .source_file = .{ .path = "borsh.zig" }, 9 | }); 10 | 11 | try b.modules.put(b.dupe("borsh"), module); 12 | 13 | const lib = b.addStaticLibrary(.{ 14 | .name = "borsh", 15 | .root_source_file = .{ .path = "borsh.zig" }, 16 | .target = target, 17 | .optimize = optimize, 18 | }); 19 | 20 | b.installArtifact(lib); 21 | 22 | const tests = b.addTest(.{ 23 | .root_source_file = .{ .path = "borsh.zig" }, 24 | .target = target, 25 | .optimize = optimize, 26 | }); 27 | 28 | const run_tests = b.addRunArtifact(tests); 29 | 30 | const test_step = b.step("test", "Run unit tests"); 31 | test_step.dependOn(&run_tests.step); 32 | } 33 | --------------------------------------------------------------------------------