From 025f2983f83ad5a7b235de04adf5ddb80b4a3ea3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 8 Feb 2024 22:23:40 +0100 Subject: [PATCH] Initial Release --- LICENSE | 21 + build.zig | 125 ++++++ build.zig.zon | 22 ++ src/cbor.zig | 914 ++++++++++++++++++++++++++++++++++++++++++++ test/tests_cbor.zig | 435 +++++++++++++++++++++ 5 files changed, 1517 insertions(+) create mode 100644 LICENSE create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/cbor.zig create mode 100644 test/tests_cbor.zig diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..92d6792 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 CJ van den Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..e49eac7 --- /dev/null +++ b/build.zig @@ -0,0 +1,125 @@ +const std = @import("std"); + +const CrossTarget = std.zig.CrossTarget; + +const cppflags = [_][]const u8{ + "-fcolor-diagnostics", + "-std=c++20", + "-Wall", + "-Wextra", + "-Werror", + "-Wno-unqualified-std-cast-call", + "-Wno-bitwise-instead-of-logical", //for notcurses + "-fno-sanitize=undefined", + "-gen-cdb-fragment-path", + ".cache/cdb", +}; + +pub fn build(b: *std.Build) void { + const enable_tracy_option = b.option(bool, "enable_tracy", "Enable tracy client library (default: no)"); + const tracy_enabled = if (enable_tracy_option) |enabled| enabled else false; + + const options = b.addOptions(); + options.addOption(bool, "enable_tracy", tracy_enabled); + + const options_mod = options.createModule(); + + const target = b.standardTargetOptions(.{ + .default_target = if (tracy_enabled) + CrossTarget.parse(.{ .arch_os_abi = "native-native-gnu" }) catch unreachable + else + CrossTarget.parse(.{ .arch_os_abi = "native-native-musl" }) catch unreachable, + }); + const optimize = b.standardOptimizeOption(.{}); + const mode = .{ .target = target, .optimize = optimize }; + + const asio_dep = b.dependency("asio", mode); + const tracy_dep = if (tracy_enabled) b.dependency("tracy", mode) else undefined; + + const lib = b.addStaticLibrary(.{ + .name = "thespian", + .target = target, + .optimize = optimize, + }); + if (tracy_enabled) { + lib.defineCMacro("TRACY_ENABLE", null); + lib.defineCMacro("TRACY_CALLSTACK", null); + } + lib.addIncludePath(.{ .path = "src" }); + lib.addIncludePath(.{ .path = "include" }); + lib.addCSourceFiles(.{ .files = &[_][]const u8{ + "src/backtrace.cpp", + "src/c/context.cpp", + "src/c/env.cpp", + "src/c/file_descriptor.cpp", + "src/c/handle.cpp", + "src/c/instance.cpp", + "src/c/metronome.cpp", + "src/c/signal.cpp", + "src/c/timeout.cpp", + "src/c/trace.cpp", + "src/cbor.cpp", + "src/executor_asio.cpp", + "src/hub.cpp", + "src/instance.cpp", + "src/trace.cpp", + }, .flags = &cppflags }); + if (tracy_enabled) { + lib.linkLibrary(tracy_dep.artifact("tracy")); + } + lib.linkLibrary(asio_dep.artifact("asio")); + lib.linkLibCpp(); + b.installArtifact(lib); + + const cbor_mod = b.addModule("cbor", .{ + .root_source_file = .{ .path = "src/cbor.zig" }, + }); + + const thespian_mod = b.addModule("thespian", .{ + .root_source_file = .{ .path = "src/thespian.zig" }, + .imports = &.{ + .{ .name = "cbor", .module = cbor_mod }, + }, + }); + thespian_mod.addIncludePath(.{ .path = "include" }); + thespian_mod.linkLibrary(lib); + + const tests = b.addTest(.{ + .root_source_file = .{ .path = "test/tests.zig" }, + .target = target, + .optimize = optimize, + }); + + tests.root_module.addImport("build_options", options_mod); + tests.root_module.addImport("cbor", cbor_mod); + tests.root_module.addImport("thespian", thespian_mod); + tests.addIncludePath(.{ .path = "test" }); + tests.addIncludePath(.{ .path = "src" }); + tests.addIncludePath(.{ .path = "include" }); + tests.addCSourceFiles(.{ .files = &[_][]const u8{ + "test/cbor_match.cpp", + "test/debug.cpp", + "test/endpoint_unx.cpp", + "test/endpoint_tcp.cpp", + "test/hub_filter.cpp", + "test/ip_tcp_client_server.cpp", + "test/ip_udp_echo.cpp", + "test/metronome_test.cpp", + "test/perf_cbor.cpp", + "test/perf_hub.cpp", + "test/perf_ring.cpp", + "test/perf_spawn.cpp", + "test/spawn_exit.cpp", + "test/tests.cpp", + "test/timeout_test.cpp", + }, .flags = &cppflags }); + tests.linkLibrary(lib); + tests.linkLibrary(asio_dep.artifact("asio")); + tests.linkLibCpp(); + b.installArtifact(tests); + + const test_run_cmd = b.addRunArtifact(tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&test_run_cmd.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..be9fe51 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,22 @@ +.{ + .name = "thespian", + .version = "0.0.1", + + .dependencies = .{ + .asio = .{ + .url = "https://github.com/kassane/asio/archive/5fc70d29f458f5b596a20bdf3d638294eef91478.tar.gz", + .hash = "1220926d7e934fcb9e4e55f25e24d17613fbe9b145798581e0ae7ea3679b37f4e44d", + }, + .tracy = .{ + .url = "https://github.com/neurocyte/zig-tracy/archive/d2113e7d778ebe7a063e95b5182ff145343aac38.tar.gz", + .hash = "122005c37f1324dcdd00600f266b64f0a66a73428550015ece51d52ae40a552608d1", + }, + }, + .paths = .{ + "include", + "src", + "test", + "build.zig", + "build.zig.zon", + }, +} diff --git a/src/cbor.zig b/src/cbor.zig new file mode 100644 index 0000000..8008533 --- /dev/null +++ b/src/cbor.zig @@ -0,0 +1,914 @@ +const std = @import("std"); + +const eql = std.mem.eql; +const bufPrint = std.fmt.bufPrint; +const fixedBufferStream = std.io.fixedBufferStream; +const maxInt = std.math.maxInt; +const minInt = std.math.minInt; +const json = std.json; +const fba = std.heap.FixedBufferAllocator; + +pub const CborError = error{ + CborIntegerTooLarge, + CborIntegerTooSmall, + CborInvalidType, + CborTooShort, + OutOfMemory, +}; + +pub const CborJsonError = error{ + BufferUnderrun, + CborIntegerTooLarge, + CborIntegerTooSmall, + CborInvalidType, + CborTooShort, + CborUnsupportedType, + NoSpaceLeft, + OutOfMemory, + Overflow, + SyntaxError, + UnexpectedEndOfInput, +}; + +const cbor_magic_null: u8 = 0xf6; +const cbor_magic_true: u8 = 0xf5; +const cbor_magic_false: u8 = 0xf4; + +const cbor_magic_type_array: u8 = 4; +const cbor_magic_type_map: u8 = 5; + +const value_type = enum(u8) { + number, + bytes, + string, + array, + map, + tag, + boolean, + null, + any, + more, + unknown, +}; +pub const number = value_type.number; +pub const bytes = value_type.bytes; +pub const string = value_type.string; +pub const array = value_type.array; +pub const map = value_type.map; +pub const tag = value_type.tag; +pub const boolean = value_type.boolean; +pub const null_ = value_type.null; +pub const any = value_type.any; +pub const more = value_type.more; + +const null_value_buf = [_]u8{0xF6}; +pub const null_value: []const u8 = &null_value_buf; + +pub fn isNull(val: []const u8) bool { + return eql(u8, val, null_value); +} + +fn isAny(value: anytype) bool { + return if (comptime @TypeOf(value) == value_type) value == value_type.any else false; +} + +fn isMore(value: anytype) bool { + return if (comptime @TypeOf(value) == value_type) value == value_type.more else false; +} + +fn write(writer: anytype, value: u8) @TypeOf(writer).Error!void { + _ = try writer.write(&[_]u8{value}); +} + +fn writeTypedVal(writer: anytype, type_: u8, value: u64) @TypeOf(writer).Error!void { + const t: u8 = type_ << 5; + if (value < 24) { + try write(writer, t | @as(u8, @truncate(value))); + } else if (value < 256) { + try write(writer, t | 24); + try write(writer, @as(u8, @truncate(value))); + } else if (value < 65536) { + try write(writer, t | 25); + try write(writer, @as(u8, @truncate(value >> 8))); + try write(writer, @as(u8, @truncate(value))); + } else if (value < 4294967296) { + try write(writer, t | 26); + try write(writer, @as(u8, @truncate(value >> 24))); + try write(writer, @as(u8, @truncate(value >> 16))); + try write(writer, @as(u8, @truncate(value >> 8))); + try write(writer, @as(u8, @truncate(value))); + } else { + try write(writer, t | 27); + try write(writer, @as(u8, @truncate(value >> 56))); + try write(writer, @as(u8, @truncate(value >> 48))); + try write(writer, @as(u8, @truncate(value >> 40))); + try write(writer, @as(u8, @truncate(value >> 32))); + try write(writer, @as(u8, @truncate(value >> 24))); + try write(writer, @as(u8, @truncate(value >> 16))); + try write(writer, @as(u8, @truncate(value >> 8))); + try write(writer, @as(u8, @truncate(value))); + } +} + +pub fn writeArrayHeader(writer: anytype, sz: usize) @TypeOf(writer).Error!void { + return writeTypedVal(writer, cbor_magic_type_array, sz); +} + +pub fn writeMapHeader(writer: anytype, sz: usize) @TypeOf(writer).Error!void { + return writeTypedVal(writer, cbor_magic_type_map, sz); +} + +pub fn writeArray(writer: anytype, args: anytype) @TypeOf(writer).Error!void { + const args_type_info = @typeInfo(@TypeOf(args)); + if (args_type_info != .Struct) @compileError("expected tuple or struct argument"); + const fields_info = args_type_info.Struct.fields; + try writeArrayHeader(writer, fields_info.len); + inline for (fields_info) |field_info| + try writeValue(writer, @field(args, field_info.name)); +} + +fn writeI64(writer: anytype, value: i64) @TypeOf(writer).Error!void { + return if (value < 0) + writeTypedVal(writer, 1, @as(u64, @bitCast(-(value + 1)))) + else + writeTypedVal(writer, 0, @as(u64, @bitCast(value))); +} + +fn writeU64(writer: anytype, value: u64) @TypeOf(writer).Error!void { + return writeTypedVal(writer, 0, value); +} + +fn writeString(writer: anytype, s: []const u8) @TypeOf(writer).Error!void { + try writeTypedVal(writer, 3, s.len); + _ = try writer.write(s); +} + +fn writeBool(writer: anytype, value: bool) @TypeOf(writer).Error!void { + return write(writer, if (value) cbor_magic_true else cbor_magic_false); +} + +fn writeNull(writer: anytype) @TypeOf(writer).Error!void { + return write(writer, cbor_magic_null); +} + +fn writeErrorset(writer: anytype, err: anyerror) @TypeOf(writer).Error!void { + var buf: [256]u8 = undefined; + const errmsg = try bufPrint(&buf, "error.{s}", .{@errorName(err)}); + return writeString(writer, errmsg); +} + +pub fn writeValue(writer: anytype, value: anytype) @TypeOf(writer).Error!void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .Int, .ComptimeInt => return if (T == u64) writeU64(writer, value) else writeI64(writer, @intCast(value)), + .Bool => return writeBool(writer, value), + .Optional => return if (value) |v| writeValue(writer, v) else writeNull(writer), + .ErrorUnion => return if (value) |v| writeValue(writer, v) else |err| writeValue(writer, err), + .ErrorSet => return writeErrorset(writer, value), + .Union => |info| { + if (info.tag_type) |TagType| { + comptime var v = void; + inline for (info.fields) |u_field| { + if (value == @field(TagType, u_field.name)) + v = @field(value, u_field.name); + } + try writeArray(writer, .{ + @typeName(T), + @tagName(@as(TagType, value)), + v, + }); + } else { + try writeArray(writer, .{@typeName(T)}); + } + }, + .Struct => |info| { + if (info.is_tuple) { + if (info.fields.len == 0) return writeNull(writer); + try writeArrayHeader(writer, info.fields.len); + inline for (info.fields) |f| + try writeValue(writer, @field(value, f.name)); + } else { + if (info.fields.len == 0) return writeNull(writer); + try writeMapHeader(writer, info.fields.len); + inline for (info.fields) |f| { + try writeString(writer, f.name); + try writeValue(writer, @field(value, f.name)); + } + } + }, + .Pointer => |ptr_info| switch (ptr_info.size) { + .One => return writeValue(writer, value.*), + .Many, .C => @compileError("cannot write type '" ++ @typeName(T) ++ "' to cbor stream"), + .Slice => { + if (ptr_info.child == u8) return writeString(writer, value); + if (value.len == 0) return writeNull(writer); + try writeArrayHeader(writer, value.len); + for (value) |elem| + try writeValue(writer, elem); + }, + }, + .Array => |info| { + if (info.child == u8) return writeString(writer, &value); + if (value.len == 0) return writeNull(writer); + try writeArrayHeader(writer, value.len); + for (value) |elem| + try writeValue(writer, elem); + }, + .Vector => |info| { + try writeArrayHeader(writer, info.len); + var i: usize = 0; + while (i < info.len) : (i += 1) { + try writeValue(writer, value[i]); + } + }, + .Null => try writeNull(writer), + else => @compileError("cannot write type '" ++ @typeName(T) ++ "' to cbor stream"), + } +} + +pub fn fmt(buf: []u8, value: anytype) []const u8 { + var stream = fixedBufferStream(buf); + writeValue(stream.writer(), value) catch unreachable; + return stream.getWritten(); +} + +const CborType = struct { type: u8, minor: u5, major: u3 }; + +pub fn decodeType(iter: *[]const u8) error{CborTooShort}!CborType { + if (iter.len < 1) + return error.CborTooShort; + const type_: u8 = iter.*[0]; + const bits: packed struct { minor: u5, major: u3 } = @bitCast(type_); + iter.* = iter.*[1..]; + return .{ .type = type_, .minor = bits.minor, .major = bits.major }; +} + +fn decodeUIntLengthRecurse(iter: *[]const u8, length: usize, acc: u64) !u64 { + if (iter.len < 1) + return error.CborTooShort; + const v: u8 = iter.*[0]; + iter.* = iter.*[1..]; + var i = acc | v; + if (length == 1) + return i; + i <<= 8; + // return @call(.always_tail, decodeUIntLengthRecurse, .{ iter, length - 1, i }); FIXME: @call(.always_tail) seems broken as of 0.11.0-dev.2964+e9cbdb2cf + return decodeUIntLengthRecurse(iter, length - 1, i); +} + +fn decodeUIntLength(iter: *[]const u8, length: usize) !u64 { + return decodeUIntLengthRecurse(iter, length, 0); +} + +fn decodePInt(iter: *[]const u8, minor: u5) !u64 { + if (minor < 24) return minor; + return switch (minor) { + 24 => decodeUIntLength(iter, 1), // 1 byte + 25 => decodeUIntLength(iter, 2), // 2 byte + 26 => decodeUIntLength(iter, 4), // 4 byte + 27 => decodeUIntLength(iter, 8), // 8 byte + else => error.CborInvalidType, + }; +} + +fn decodeNInt(iter: *[]const u8, minor: u5) CborError!i64 { + return -@as(i64, @intCast(try decodePInt(iter, minor) + 1)); +} + +pub fn decodeMapHeader(iter: *[]const u8) CborError!usize { + const t = try decodeType(iter); + if (t.type == cbor_magic_null) + return 0; + if (t.major != 5) + return error.CborInvalidType; + return decodePInt(iter, t.minor); +} + +pub fn decodeArrayHeader(iter: *[]const u8) CborError!usize { + const t = try decodeType(iter); + if (t.type == cbor_magic_null) + return 0; + if (t.major != 4) + return error.CborInvalidType; + return decodePInt(iter, t.minor); +} + +fn decodeString(iter_: *[]const u8, minor: u5) CborError![]const u8 { + var iter = iter_.*; + const len = try decodePInt(&iter, minor); + if (iter.len < len) + return error.CborTooShort; + const s = iter[0..len]; + iter = iter[len..]; + iter_.* = iter; + return s; +} + +fn decodeBytes(iter: *[]const u8, minor: u5) CborError![]const u8 { + return decodeString(iter, minor); +} + +fn decodeJsonArray(iter_: *[]const u8, minor: u5, arr: *json.Array) CborError!bool { + var iter = iter_.*; + var n = try decodePInt(&iter, minor); + while (n > 0) { + const value = try arr.addOne(); + if (!try matchJsonValue(&iter, value, arr.allocator)) + return false; + n -= 1; + } + iter_.* = iter; + return true; +} + +fn decodeJsonObject(iter_: *[]const u8, minor: u5, obj: *json.ObjectMap) CborError!bool { + var iter = iter_.*; + var n = try decodePInt(&iter, minor); + while (n > 0) { + var key: []u8 = undefined; + var value: json.Value = .null; + + if (!try matchString(&iter, &key)) + return false; + if (!try matchJsonValue(&iter, &value, obj.allocator)) + return false; + + _ = try obj.getOrPutValue(key, value); + n -= 1; + } + iter_.* = iter; + return true; +} + +pub fn matchInt(comptime T: type, iter_: *[]const u8, val: *T) CborError!bool { + var iter = iter_.*; + const t = try decodeType(&iter); + val.* = switch (t.major) { + 0 => blk: { // positive integer + const v = try decodePInt(&iter, t.minor); + if (v > maxInt(T)) + return error.CborIntegerTooLarge; + break :blk @intCast(v); + }, + 1 => blk: { // negative integer + const v = try decodeNInt(&iter, t.minor); + if (v < minInt(T)) + return error.CborIntegerTooSmall; + break :blk @intCast(v); + }, + + else => return false, + }; + iter_.* = iter; + return true; +} + +pub fn matchIntValue(comptime T: type, iter: *[]const u8, val: T) CborError!bool { + var v: T = 0; + return if (try matchInt(T, iter, &v)) v == val else false; +} + +pub fn matchBool(iter_: *[]const u8, v: *bool) CborError!bool { + var iter = iter_.*; + const t = try decodeType(&iter); + if (t.major == 7) { // special + if (t.type == cbor_magic_false) { + v.* = false; + iter_.* = iter; + return true; + } + if (t.type == cbor_magic_true) { + v.* = true; + iter_.* = iter; + return true; + } + } + return false; +} + +fn matchBoolValue(iter: *[]const u8, val: bool) CborError!bool { + var v: bool = false; + return if (try matchBool(iter, &v)) v == val else false; +} + +fn skipString(iter: *[]const u8, minor: u5) CborError!void { + const len = try decodePInt(iter, minor); + if (iter.len < len) + return error.CborTooShort; + iter.* = iter.*[len..]; +} + +fn skipBytes(iter: *[]const u8, minor: u5) CborError!void { + return skipString(iter, minor); +} + +fn skipArray(iter: *[]const u8, minor: u5) CborError!void { + var len = try decodePInt(iter, minor); + while (len > 0) { + try skipValue(iter); + len -= 1; + } +} + +fn skipMap(iter: *[]const u8, minor: u5) CborError!void { + var len = try decodePInt(iter, minor); + len *= 2; + while (len > 0) { + try skipValue(iter); + len -= 1; + } +} + +pub fn skipValue(iter: *[]const u8) CborError!void { + const t = try decodeType(iter); + try skipValueType(iter, t.major, t.minor); +} + +fn skipValueType(iter: *[]const u8, major: u3, minor: u5) CborError!void { + switch (major) { + 0 => { // positive integer + _ = try decodePInt(iter, minor); + }, + 1 => { // negative integer + _ = try decodeNInt(iter, minor); + }, + 2 => { // bytes + try skipBytes(iter, minor); + }, + 3 => { // string + try skipString(iter, minor); + }, + 4 => { // array + try skipArray(iter, minor); + }, + 5 => { // map + try skipMap(iter, minor); + }, + 6 => { // tag + return error.CborInvalidType; + }, + 7 => { // special + return; + }, + } +} + +fn matchType(iter_: *[]const u8, v: *value_type) CborError!bool { + var iter = iter_.*; + const t = try decodeType(&iter); + try skipValueType(&iter, t.major, t.minor); + switch (t.major) { + 0, 1 => v.* = value_type.number, // positive integer or negative integer + 2 => v.* = value_type.bytes, // bytes + 3 => v.* = value_type.string, // string + 4 => v.* = value_type.array, // array + 5 => v.* = value_type.map, // map + 7 => { // special + if (t.type == cbor_magic_null) { + v.* = value_type.null; + } else { + if (t.type == cbor_magic_false or t.type == cbor_magic_true) { + v.* = value_type.boolean; + } else { + return false; + } + } + }, + else => return false, + } + iter_.* = iter; + return true; +} + +fn matchValueType(iter: *[]const u8, t: value_type) CborError!bool { + var v: value_type = value_type.unknown; + return if (try matchType(iter, &v)) (t == value_type.any or t == v) else false; +} + +pub fn matchString(iter_: *[]const u8, val: *[]const u8) CborError!bool { + var iter = iter_.*; + const t = try decodeType(&iter); + val.* = switch (t.major) { + 2 => try decodeBytes(&iter, t.minor), // bytes + 3 => try decodeString(&iter, t.minor), // string + else => return false, + }; + iter_.* = iter; + return true; +} + +fn matchStringValue(iter: *[]const u8, lit: []const u8) CborError!bool { + var val: []const u8 = undefined; + return if (try matchString(iter, &val)) eql(u8, val, lit) else false; +} + +fn matchError(comptime T: type) noreturn { + @compileError("cannot match type '" ++ @typeName(T) ++ "' to cbor stream"); +} + +pub fn matchValue(iter: *[]const u8, value: anytype) CborError!bool { + if (@TypeOf(value) == value_type) + return matchValueType(iter, value); + const T = comptime @TypeOf(value); + if (comptime isExtractor(T)) + return value.extract(iter); + return switch (comptime @typeInfo(T)) { + .Int => return matchIntValue(T, iter, value), + .ComptimeInt => return matchIntValue(i64, iter, value), + .Bool => matchBoolValue(iter, value), + .Pointer => |info| switch (info.size) { + .One => matchValue(iter, value.*), + .Many, .C => matchError(T), + .Slice => if (info.child == u8) matchStringValue(iter, value) else matchArray(iter, value, info), + }, + .Struct => |info| if (info.is_tuple) + matchArray(iter, value, info) + else + matchError(T), + .Array => |info| if (info.child == u8) matchStringValue(iter, &value) else matchArray(iter, value, info), + else => @compileError("cannot match value type '" ++ @typeName(T) ++ "' to cbor stream"), + }; +} + +fn matchJsonValue(iter_: *[]const u8, v: *json.Value, a: std.mem.Allocator) CborError!bool { + var iter = iter_.*; + const t = try decodeType(&iter); + const ret = switch (t.major) { + 0 => ret: { // positive integer + v.* = json.Value{ .integer = @intCast(try decodePInt(&iter, t.minor)) }; + break :ret true; + }, + 1 => ret: { // negative integer + v.* = json.Value{ .integer = try decodeNInt(&iter, t.minor) }; + break :ret true; + }, + 2 => ret: { // bytes + break :ret false; + }, + 3 => ret: { // string + v.* = json.Value{ .string = try decodeString(&iter, t.minor) }; + break :ret true; + }, + 4 => ret: { // array + v.* = json.Value{ .array = json.Array.init(a) }; + break :ret try decodeJsonArray(&iter, t.minor, &v.array); + }, + 5 => ret: { // map + v.* = json.Value{ .object = json.ObjectMap.init(a) }; + break :ret try decodeJsonObject(&iter, t.minor, &v.object); + }, + 6 => ret: { // tag + break :ret false; + }, + 7 => ret: { // special + switch (t.type) { + cbor_magic_false => { + v.* = json.Value{ .bool = false }; + break :ret true; + }, + cbor_magic_true => { + v.* = json.Value{ .bool = true }; + break :ret true; + }, + cbor_magic_null => { + v.* = json.Value{ .null = {} }; + break :ret true; + }, + else => break :ret false, + } + }, + }; + if (ret) iter_.* = iter; + return ret; +} + +fn matchArrayMore(iter_: *[]const u8, n_: u64) CborError!bool { + var iter = iter_.*; + var n = n_; + while (n > 0) { + if (!try matchValue(&iter, value_type.any)) + return false; + n -= 1; + } + iter_.* = iter; + return true; +} + +fn matchArray(iter_: *[]const u8, arr: anytype, info: anytype) CborError!bool { + var iter = iter_.*; + var n = try decodeArrayHeader(&iter); + inline for (info.fields) |f| { + const value = @field(arr, f.name); + if (isMore(value)) + break; + } else if (info.fields.len != n) + return false; + inline for (info.fields) |f| { + const value = @field(arr, f.name); + if (isMore(value)) + return matchArrayMore(&iter, n); + if (n == 0) return false; + const matched = try matchValue(&iter, @field(arr, f.name)); + if (!matched) return false; + n -= 1; + } + if (n == 0) iter_.* = iter; + return n == 0; +} + +fn matchJsonObject(iter_: *[]const u8, obj: *json.ObjectMap) !bool { + var iter = iter_.*; + const t = try decodeType(&iter); + if (t.type == cbor_magic_null) + return true; + if (t.major != 5) + return error.CborInvalidType; + const ret = try decodeJsonObject(&iter, t.minor, obj); + if (ret) iter_.* = iter; + return ret; +} + +pub fn match(buf: []const u8, pattern: anytype) CborError!bool { + var iter: []const u8 = buf; + return matchValue(&iter, pattern); +} + +fn extractError(comptime T: type) noreturn { + @compileError("cannot extract type '" ++ @typeName(T) ++ "' from cbor stream"); +} + +fn hasExtractorTag(info: anytype) bool { + if (info.is_tuple) return false; + inline for (info.decls) |decl| { + if (comptime eql(u8, decl.name, "EXTRACTOR_TAG")) + return true; + } + return false; +} + +fn isExtractor(comptime T: type) bool { + return comptime switch (@typeInfo(T)) { + .Struct => |info| hasExtractorTag(info), + else => false, + }; +} + +const JsonValueExtractor = struct { + dest: *T, + const Self = @This(); + pub const EXTRACTOR_TAG = struct {}; + const T = json.Value; + + pub fn init(dest: *T) Self { + return .{ .dest = dest }; + } + + pub fn extract(self: Self, iter: *[]const u8) CborError!bool { + var null_heap_: [0]u8 = undefined; + var heap = fba.init(&null_heap_); + return matchJsonValue(iter, self.dest, heap.allocator()); + } +}; + +const JsonObjectExtractor = struct { + dest: *T, + const Self = @This(); + pub const EXTRACTOR_TAG = struct {}; + const T = json.ObjectMap; + + pub fn init(dest: *T) Self { + return .{ .dest = dest }; + } + + pub fn extract(self: Self, iter: *[]const u8) CborError!bool { + return matchJsonObject(iter, self.dest); + } +}; + +fn Extractor(comptime T: type) type { + if (T == json.Value) + return JsonValueExtractor; + if (T == json.ObjectMap) + return JsonObjectExtractor; + return struct { + dest: *T, + const Self = @This(); + pub const EXTRACTOR_TAG = struct {}; + + pub fn init(dest: *T) Self { + return .{ .dest = dest }; + } + + pub fn extract(self: Self, iter: *[]const u8) CborError!bool { + switch (comptime @typeInfo(T)) { + .Int, .ComptimeInt => return matchInt(T, iter, self.dest), + .Bool => return matchBool(iter, self.dest), + .Pointer => |ptr_info| switch (ptr_info.size) { + .Slice => { + if (ptr_info.child == u8) return matchString(iter, self.dest) else extractError(T); + }, + else => extractError(T), + }, + else => extractError(T), + } + } + }; +} + +fn ExtractorType(comptime T: type) type { + const T_type_info = @typeInfo(T); + if (T_type_info != .Pointer) @compileError("extract requires a pointer argument"); + return Extractor(T_type_info.Pointer.child); +} + +pub fn extract(dest: anytype) ExtractorType(@TypeOf(dest)) { + comptime { + if (!isExtractor(ExtractorType(@TypeOf(dest)))) + @compileError("isExtractor self check failed for " ++ @typeName(ExtractorType(@TypeOf(dest)))); + } + return ExtractorType(@TypeOf(dest)).init(dest); +} + +const CborExtractor = struct { + dest: *[]const u8, + const Self = @This(); + pub const EXTRACTOR_TAG = struct {}; + + pub fn init(dest: *[]const u8) Self { + return .{ .dest = dest }; + } + + pub fn extract(self: Self, iter: *[]const u8) CborError!bool { + const b = iter.*; + try skipValue(iter); + self.dest.* = b[0..(b.len - iter.len)]; + return true; + } +}; + +pub fn extract_cbor(dest: *[]const u8) CborExtractor { + return CborExtractor.init(dest); +} + +pub fn JsonStream(comptime T: type) type { + return struct { + const Writer = T.Writer; + const JsonWriter = json.WriteStream(Writer, .{ .checked_to_fixed_depth = 256 }); + + fn jsonWriteArray(w: *JsonWriter, iter: *[]const u8, minor: u5) !void { + var count = try decodePInt(iter, minor); + try w.beginArray(); + while (count > 0) : (count -= 1) { + try jsonWriteValue(w, iter); + } + try w.endArray(); + } + + fn jsonWriteMap(w: *JsonWriter, iter: *[]const u8, minor: u5) !void { + var count = try decodePInt(iter, minor); + try w.beginObject(); + while (count > 0) : (count -= 1) { + const t = try decodeType(iter); + if (t.major != 3) return error.CborInvalidType; + try w.objectField(try decodeString(iter, t.minor)); + try jsonWriteValue(w, iter); + } + try w.endObject(); + } + + pub fn jsonWriteValue(w: *JsonWriter, iter: *[]const u8) (CborJsonError || Writer.Error)!void { + const t = try decodeType(iter); + if (t.type == cbor_magic_false) + return w.write(false); + if (t.type == cbor_magic_true) + return w.write(true); + if (t.type == cbor_magic_null) + return w.write(null); + return switch (t.major) { + 0 => w.write(try decodePInt(iter, t.minor)), // positive integer + 1 => w.write(try decodeNInt(iter, t.minor)), // negative integer + 2 => error.CborUnsupportedType, // bytes + 3 => w.write(try decodeString(iter, t.minor)), // string + 4 => jsonWriteArray(w, iter, t.minor), // array + 5 => jsonWriteMap(w, iter, t.minor), // map + else => error.CborInvalidType, + }; + } + }; +} + +pub fn toJson(cbor_buf: []const u8, json_buf: []u8) CborJsonError![]const u8 { + var fbs = fixedBufferStream(json_buf); + var s = json.writeStream(fbs.writer(), .{}); + var iter: []const u8 = cbor_buf; + try JsonStream(@TypeOf(fbs)).jsonWriteValue(&s, &iter); + return fbs.getWritten(); +} + +pub fn toJsonPretty(cbor_buf: []const u8, json_buf: []u8) CborJsonError![]const u8 { + var fbs = fixedBufferStream(json_buf); + var s = json.writeStream(fbs.writer(), .{ .whitespace = .indent_1 }); + var iter: []const u8 = cbor_buf; + try JsonStream(@TypeOf(fbs)).jsonWriteValue(&s, &iter); + return fbs.getWritten(); +} + +fn writeJsonValue(writer: anytype, value: json.Value) !void { + try switch (value) { + .array => |_| unreachable, + .object => |_| unreachable, + .null => writeNull(writer), + .float => |_| error.CborUnsupportedType, + inline else => |v| writeValue(writer, v), + }; +} + +fn jsonScanUntil(writer: anytype, scanner: *json.Scanner, end_token: anytype) CborJsonError!usize { + var partial = try std.BoundedArray(u8, 4096).init(0); + var count: usize = 0; + + var token = try scanner.next(); + while (token != end_token) : (token = try scanner.next()) { + count += 1; + switch (token) { + .object_begin => try writeJsonObject(writer, scanner), + .array_begin => try writeJsonArray(writer, scanner), + + .true => try writeBool(writer, true), + .false => try writeBool(writer, false), + .null => try writeNull(writer), + + .number => |v| { + try partial.appendSlice(v); + try writeJsonValue(writer, json.Value.parseFromNumberSlice(partial.slice())); + try partial.resize(0); + }, + .partial_number => |v| { + try partial.appendSlice(v); + count -= 1; + }, + + .string => |v| { + try partial.appendSlice(v); + try writeString(writer, partial.slice()); + try partial.resize(0); + }, + .partial_string => |v| { + try partial.appendSlice(v); + count -= 1; + }, + .partial_string_escaped_1 => |v| { + try partial.appendSlice(&v); + count -= 1; + }, + .partial_string_escaped_2 => |v| { + try partial.appendSlice(&v); + count -= 1; + }, + .partial_string_escaped_3 => |v| { + try partial.appendSlice(&v); + count -= 1; + }, + .partial_string_escaped_4 => |v| { + try partial.appendSlice(&v); + count -= 1; + }, + + else => return error.SyntaxError, + } + } + return count; +} + +pub const local_heap_size = 4096 * 16; + +fn writeJsonArray(writer_: anytype, scanner: *json.Scanner) CborJsonError!void { + var buf: [local_heap_size]u8 = undefined; + var stream = fixedBufferStream(&buf); + const writer = stream.writer(); + const count = try jsonScanUntil(writer, scanner, .array_end); + try writeArrayHeader(writer_, count); + try writer_.writeAll(stream.getWritten()); +} + +fn writeJsonObject(writer_: anytype, scanner: *json.Scanner) CborJsonError!void { + var buf: [local_heap_size]u8 = undefined; + var stream = fixedBufferStream(&buf); + const writer = stream.writer(); + const count = try jsonScanUntil(writer, scanner, .object_end); + try writeMapHeader(writer_, count / 2); + try writer_.writeAll(stream.getWritten()); +} + +pub fn fromJson(json_buf: []const u8, cbor_buf: []u8) ![]const u8 { + var local_heap_: [local_heap_size]u8 = undefined; + var heap = fba.init(&local_heap_); + var stream = fixedBufferStream(cbor_buf); + const writer = stream.writer(); + + var scanner = json.Scanner.initCompleteInput(heap.allocator(), json_buf); + defer scanner.deinit(); + + _ = try jsonScanUntil(writer, &scanner, .end_of_document); + return stream.getWritten(); +} diff --git a/test/tests_cbor.zig b/test/tests_cbor.zig new file mode 100644 index 0000000..6ba45ca --- /dev/null +++ b/test/tests_cbor.zig @@ -0,0 +1,435 @@ +const std = @import("std"); +const cbor_mod = @import("cbor"); + +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const expectEqualDeep = std.testing.expectEqualDeep; +const expectError = std.testing.expectError; + +const fmt = cbor_mod.fmt; +const toJson = cbor_mod.toJson; +const toJsonPretty = cbor_mod.toJsonPretty; +const fromJson = cbor_mod.fromJson; +const decodeType = cbor_mod.decodeType; +const matchInt = cbor_mod.matchInt; +const matchIntValue = cbor_mod.matchIntValue; +const matchValue = cbor_mod.matchValue; +const match = cbor_mod.match; +const isNull = cbor_mod.isNull; +const writeArrayHeader = cbor_mod.writeArrayHeader; +const writeMapHeader = cbor_mod.writeMapHeader; +const writeValue = cbor_mod.writeValue; +const extract = cbor_mod.extract; +const extract_cbor = cbor_mod.extract_cbor; + +const more = cbor_mod.more; +const any = cbor_mod.any; +const string = cbor_mod.string; +const number = cbor_mod.number; +const array = cbor_mod.array; +const map = cbor_mod.map; + +test "cbor simple" { + var buf: [128]u8 = undefined; + try expectEqualDeep( + fmt(&buf, .{ "five", 5, "four", 4, .{ "three", 3 } }), + &[_]u8{ 0x85, 0x64, 0x66, 0x69, 0x76, 0x65, 0x05, 0x64, 0x66, 0x6f, 0x75, 0x72, 0x04, 0x82, 0x65, 0x74, 0x68, 0x72, 0x65, 0x65, 0x03 }, + ); +} + +test "cbor exit message" { + var buf: [128]u8 = undefined; + try expectEqualDeep( + fmt(&buf, .{ "exit", "normal" }), + &[_]u8{ 0x82, 0x64, 0x65, 0x78, 0x69, 0x74, 0x66, 0x6E, 0x6F, 0x72, 0x6D, 0x61, 0x6C }, + ); +} + +test "cbor error.OutOfMemory" { + var buf: [128]u8 = undefined; + try expectEqualDeep( + fmt(&buf, .{ "exit", error.OutOfMemory }), + &[_]u8{ 0x82, 0x64, 0x65, 0x78, 0x69, 0x74, 0x71, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x2E, 0x4F, 0x75, 0x74, 0x4F, 0x66, 0x4D, 0x65, 0x6D, 0x6F, 0x72, 0x79 }, + ); +} + +test "cbor error.OutOfMemory json" { + var buf: [128]u8 = undefined; + var json_buf: [128]u8 = undefined; + const cbor = fmt(&buf, .{ "exit", error.OutOfMemory }); + try expectEqualDeep(toJson(cbor, &json_buf), + \\["exit","error.OutOfMemory"] + ); +} + +test "cbor.decodeType2" { + var buf = [_]u8{ 0x20, 0xDF }; + var iter: []const u8 = &buf; + const t = try decodeType(&iter); + try expectEqual(t.major, 1); + try expectEqual(t.minor, 0); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.decodeType3" { + var buf = [_]u8{ 0x03, 0xDF }; + var iter: []const u8 = &buf; + const t = try decodeType(&iter); + try expectEqual(t.major, 0); + try expectEqual(t.minor, 3); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.matchI64 small" { + var buf = [_]u8{ 1, 0xDF }; + var iter: []const u8 = &buf; + var val: i64 = 0; + try expect(try matchInt(i64, &iter, &val)); + try expectEqual(val, 1); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.matchI64 1byte" { + var buf = [_]u8{ 0x18, 0x1A, 0xDF }; + var iter: []const u8 = &buf; + var val: i64 = 0; + try expect(try matchInt(i64, &iter, &val)); + try expectEqual(val, 26); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.matchI64 2byte" { + var buf = [_]u8{ 0x19, 0x01, 0x07, 0xDF }; + var iter: []const u8 = &buf; + var val: i64 = 0; + try expect(try matchInt(i64, &iter, &val)); + try expectEqual(val, 263); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.matchI64 8byte" { + var buf = [_]u8{ 0x1B, 0x00, 0x00, 0xEF, 0x6F, 0xC1, 0x4A, 0x0A, 0x1F, 0xDF }; + var iter: []const u8 = &buf; + var val: i64 = 0; + try expect(try matchInt(i64, &iter, &val)); + try expectEqual(val, 263263263263263); + try expectEqual(iter[0], 0xDF); +} + +test "cbor.matchI64 error.CborIntegerTooLarge" { + var buf = [_]u8{ 0x1B, 0xA9, 0x0A, 0xDE, 0x0D, 0x4E, 0x2B, 0x8A, 0x1F, 0xDF }; + var iter: []const u8 = &buf; + var val: i64 = 0; + const result = matchInt(i64, &iter, &val); + try expectError(error.CborIntegerTooLarge, result); +} + +test "cbor.matchI64 error.CborTooShort" { + var buf = [_]u8{ 0x19, 0x01 }; + var iter: []const u8 = &buf; + var val: i64 = 0; + const result = matchInt(i64, &iter, &val); + try expectError(error.CborTooShort, result); +} + +test "cbor.matchI64Value" { + var buf = [_]u8{ 7, 0xDF }; + var iter: []const u8 = &buf; + try expect(try matchIntValue(i64, &iter, 7)); +} + +test "cbor.matchValue(i64)" { + var buf: [128]u8 = undefined; + var iter = fmt(&buf, 7); + try expect(try matchValue(&iter, 7)); +} + +test "cbor.matchValue(i64) multi" { + var buf: [128]u8 = undefined; + const iter = fmt(&buf, 7); + const iter2 = fmt(buf[iter.len..], 8); + var iter3 = buf[0 .. iter.len + iter2.len]; + try expect(try matchValue(&iter3, 7)); + try expect(try matchValue(&iter3, 8)); +} + +test "cbor.match(.{i64...})" { + var buf: [128]u8 = undefined; + const v = .{ 5, 4, 3, 123456, 234567890 }; + const m = fmt(&buf, v); + try expect(try match(m, v)); +} + +test "cbor.match(.{i64... more})" { + var buf: [128]u8 = undefined; + const v = .{ 5, 4, 3, 123456, 234567890, 6, 5, 4, 3, 2, 1 }; + const m = fmt(&buf, v); + try expect(try match(m, .{ 5, 4, 3, more })); +} + +test "cbor.match(.{any, i64... more})" { + var buf: [128]u8 = undefined; + const v = .{ "cbor", 4, 3, 123456, 234567890, 6, 5, 4, 3, 2, 1 }; + const m = fmt(&buf, v); + try expect(try match(m, .{ any, 4, 3, more })); +} + +test "cbor.match(.{types...})" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ "three", 3 } }; + const m = fmt(&buf, v); + try expect(try match(m, .{ string, number, string, number, array })); + + try expect(!try match(m, .{ string, number, string, number })); + try expect(!try match(m, .{ number, string, number, array, any })); + try expect(try match(m, .{ any, number, string, number, any })); + + try expect(try match(m, .{ "five", number, string, 4, any })); + try expect(try match(m, .{ "five", 5, "four", 4, array })); + try expect(try match(m, .{ "five", 5, more })); + try expect(try match(m, .{ "five", 5, "four", 4, array, more })); + try expect(!try match(m, .{ "five", 5, "four", 4, array, any })); + try expect(!try match(m, .{ "four", 5, "five", 4, array, any })); + try expect(try match(m, .{ "five", 5, "four", 4, .{ "three", 3 } })); +} + +test "cbor.nulls" { + var buf: [128]u8 = undefined; + try expect(isNull(fmt(&buf, .{}))); + try expect(!isNull(fmt(&buf, .{1}))); +} + +test "cbor.stream_writer" { + var buf: [128]u8 = undefined; + var stream = std.io.fixedBufferStream(&buf); + const writer = stream.writer(); + try writeArrayHeader(writer, 5); + try writeValue(writer, "five"); + try writeValue(writer, 5); + try writeValue(writer, "four"); + try writeValue(writer, 4); + try writeArrayHeader(writer, 2); + try writeValue(writer, "three"); + try writeValue(writer, 3); + try expect(try match(stream.getWritten(), .{ "five", 5, "four", 4, .{ "three", 3 } })); +} + +test "cbor.stream_object_writer" { + var buf: [128]u8 = undefined; + var stream = std.io.fixedBufferStream(&buf); + const writer = stream.writer(); + try writeMapHeader(writer, 3); + try writeValue(writer, "five"); + try writeValue(writer, 5); + try writeValue(writer, "four"); + try writeValue(writer, 4); + try writeValue(writer, "three"); + try writeValue(writer, 3); + const obj = stream.getWritten(); + var json_buf: [128]u8 = undefined; + const json = try toJson(obj, &json_buf); + try expectEqualDeep(json, + \\{"five":5,"four":4,"three":3} + ); +} + +test "cbor.match_bool" { + var buf: [128]u8 = undefined; + const m = fmt(&buf, .{ false, true, 5, "five" }); + try expect(try match(m, .{ false, true, 5, "five" })); + try expect(!try match(m, .{ true, false, 5, "five" })); +} + +test "cbor.extract_match" { + var buf: [128]u8 = undefined; + var t: bool = false; + const extractor = extract(&t); + var iter = fmt(&buf, true); + try expect(try extractor.extract(&iter)); + try expect(t); + iter = fmt(&buf, false); + try expect(try extractor.extract(&iter)); + try expect(!t); + + const m = fmt(&buf, .{ false, true, 5, "five" }); + try expect(try match(m, .{ extract(&t), true, 5, "five" })); + try expect(!t); + try expect(try match(m, .{ false, extract(&t), 5, "five" })); + try expect(t); + + var i: i64 = undefined; + try expect(try match(m, .{ false, true, extract(&i), "five" })); + try expect(i == 5); + + var u: u64 = undefined; + try expect(try match(m, .{ false, true, extract(&u), "five" })); + try expect(u == 5); + + var s: []const u8 = undefined; + try expect(try match(m, .{ false, true, 5, extract(&s) })); + try expect(std.mem.eql(u8, "five", s)); +} + +test "cbor.extract_cbor" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ "three", 3 } }; + const m = fmt(&buf, v); + + try expect(try match(m, .{ "five", 5, "four", 4, .{ "three", 3 } })); + + var sub: []const u8 = undefined; + try expect(try match(m, .{ "five", 5, "four", 4, extract_cbor(&sub) })); + try expect(try match(sub, .{ "three", 3 })); +} + +test "cbor.extract_nested" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ "three", 3 } }; + const m = fmt(&buf, v); + + var u: u64 = undefined; + try expect(try match(m, .{ "five", 5, "four", 4, .{ "three", extract(&u) } })); + try expect(u == 3); +} + +test "cbor.match_map" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ .three = 3 } }; + const m = fmt(&buf, v); + try expect(try match(m, .{ "five", 5, "four", 4, map })); +} + +test "cbor.extract_map_cbor" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ .three = 3 } }; + const m = fmt(&buf, v); + var map_cbor: []const u8 = undefined; + try expect(try match(m, .{ "five", 5, "four", 4, extract_cbor(&map_cbor) })); + var json_buf: [256]u8 = undefined; + const json = try toJson(map_cbor, &json_buf); + try expectEqualDeep(json, + \\{"three":3} + ); +} + +test "cbor.extract_map" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ .three = 3 } }; + const m = fmt(&buf, v); + var obj = std.json.ObjectMap.init(std.testing.allocator); + defer obj.deinit(); + try expect(try match(m, .{ "five", 5, "four", 4, extract(&obj) })); + try expect(obj.contains("three")); + try expectEqual(obj.get("three").?, std.json.Value{ .integer = 3 }); +} + +test "cbor.extract_map_map" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ .three = 3, .child = .{ .two = 2, .one = .{ 1, 2, 3, true, false, null } } } }; + const m = fmt(&buf, v); + var a = std.heap.ArenaAllocator.init(std.testing.allocator); + defer a.deinit(); + var obj = std.json.ObjectMap.init(a.allocator()); + defer obj.deinit(); + try expect(try match(m, .{ "five", 5, "four", 4, extract(&obj) })); + try expect(obj.contains("three")); + try expectEqual(obj.get("three").?, std.json.Value{ .integer = 3 }); + var child = obj.get("child").?.object; + try expectEqual(child.get("two"), std.json.Value{ .integer = 2 }); + + var json_buf: [256]u8 = undefined; + const json = try toJson(m, &json_buf); + try expectEqualDeep(json, + \\["five",5,"four",4,{"three":3,"child":{"two":2,"one":[1,2,3,true,false,null]}}] + ); +} + +test "cbor.extract_value" { + var buf: [128]u8 = undefined; + const v = .{ "five", 5, "four", 4, .{ .three = 3 } }; + const m = fmt(&buf, v); + var value = std.json.Value{ .null = {} }; + var value_int = std.json.Value{ .null = {} }; + try expect(try match(m, .{ "five", 5, extract(&value), extract(&value_int), any })); + try expectEqualDeep(value.string, "four"); + try expectEqualDeep(value_int.integer, 4); +} + +test "cbor.match_more_nested" { + var buf: [128]u8 = undefined; + const v = .{ .{124}, .{ "three", 3 } }; + const m = fmt(&buf, v); + try expect(try match(m, .{ .{more}, .{more} })); +} + +test "cbor.extract_number_limits" { + var buf: [128]u8 = undefined; + const bigint: u64 = 18446744073709551615; + var u: u64 = undefined; + var i: i64 = undefined; + const m = fmt(&buf, bigint); + try expect(try match(m, extract(&u))); + try expectError(error.CborIntegerTooLarge, match(m, extract(&i))); +} + +test "cbor.toJson" { + var buf: [128]u8 = undefined; + var json_buf: [128]u8 = undefined; + const a = fmt(&buf, .{ "string\r\n", "\tstring\t", "\r\nstring" }); + const json = try toJson(a, &json_buf); + try expect(try match(a, .{ "string\r\n", "\tstring\t", "\r\nstring" })); + try expectEqualDeep(json, + \\["string\r\n","\tstring\t","\r\nstring"] + ); +} + +test "cbor.toJson_object" { + var buf: [128]u8 = undefined; + var json_buf: [128]u8 = undefined; + const a = fmt(&buf, .{ .five = 5, .four = 4, .three = 3 }); + const json = try toJson(a, &json_buf); + try expectEqualDeep(json, + \\{"five":5,"four":4,"three":3} + ); +} + +test "cbor.toJsonPretty_object" { + var buf: [128]u8 = undefined; + var json_buf: [128]u8 = undefined; + const a = fmt(&buf, .{ .five = 5, .four = 4, .three = 3 }); + const json = try toJsonPretty(a, &json_buf); + try expectEqualDeep(json, + \\{ + \\ "five": 5, + \\ "four": 4, + \\ "three": 3 + \\} + ); +} + +test "cbor.fromJson_small" { + var cbor_buf: [128]u8 = undefined; + const json_buf: []const u8 = + \\[12345] + ; + const cbor = try fromJson(json_buf, &cbor_buf); + try expect(try match(cbor, .{12345})); +} + +test "cbor.fromJson" { + var cbor_buf: [128]u8 = undefined; + const json_buf: []const u8 = + \\["string\r\n","\tstring\t","\r\nstring",12345] + ; + const cbor = try fromJson(json_buf, &cbor_buf); + try expect(try match(cbor, .{ "string\r\n", "\tstring\t", "\r\nstring", 12345 })); +} + +test "cbor.fromJson_object" { + var cbor_buf: [128]u8 = undefined; + const json_buf: []const u8 = + \\{"five":5,"four":4,"three":3} + ; + const cbor = try fromJson(json_buf, &cbor_buf); + try expect(try match(cbor, map)); +}