The Concise Binary Object Representation (CBOR) is a data format whose design goals include the possibility of extremely small code size, fairly small message size, and extensibility without the need for version negotiation (RFC8949). It is used in different protocols like CTAP and WebAuthn (FIDO2).
To use this library in your own project just add it as a submodule, e.g.:
your-project$ mkdir libs
your-project$ git submodule add https://github.com/r4gus/zbor.git libs/zbor
Then add the following line to your build.zig
file.
exe.addPackagePath("zbor", "libs/zbor/src/main.zig");
This library lets you inspect and parse CBOR data without having to allocate additional memory.
Note: This library is not mature and probably still has bugs. If you encounter any errors please open an issue.
To inspect CBOR data you must first create a new DataItem
.
const cbor = @import("zbor");
const di = DataItem.new("\x1b\xff\xff\xff\xff\xff\xff\xff\xff") catch {
// handle the case that the given data is malformed
};
DataItem.new()
will check if the given data is well-formed before returning a DataItem
. The data is well formed if it's syntactically correct and no bytes are left in the input after parsing (see RFC 8949 Appendix C).
To check the type of the given DataItem
use the getType()
function.
std.debug.assert(di.getType() == .Int);
Possible types include Int
(major type 0 and 1) ByteString
(major type 2), TextString
(major type 3), Array
(major type 4), Map
(major type 5), Tagged
(major type 6) and Float
(major type 7).
Based on the given type you can the access the underlying value.
std.debug.assert(di.int().? == 18446744073709551615);
All getter functions return either a value or null
. You can use a pattern like if (di.int()) |v| v else return error.Oops;
to access the value in a safe way. If you've used DataItem.new()
and know the type of the data item, you should be safe to just do di.int().?
.
The following getter functions are supported:
int
- returns?i65
string
- returns?[]const u8
array
- returns?ArrayIterator
map
- returns?MapIterator
simple
- returns?u8
float
- returns?f64
tagged
- returns?Tag
boolean
- returns?bool
The functions array
and map
will return an iterator. Every time you
call next()
you will either get a DataItem
/ Pair
or null
.
const di = DataItem.new("\x98\x19\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x18\x18\x19");
var iter = di.array().?;
while (iter.next()) |value| {
_ = value;
// doe something
}
You can serialize Zig objects into CBOR using the stringify()
function.
const allocator = std.testing.allocator;
var str = std.ArrayList(u8).init(allocator);
defer str.deinit();
const Info = struct {
versions: []const []const u8,
};
const i = Info{
.versions = &.{"FIDO_2_0"},
};
try stringify(i, .{}, str.writer());
Note: Compile time floats are always encoded as single precision floats (f32). Please use
@floatCast
before passing a float tostringify()
.
u8
slices with sentinel terminator (e.g. const x: [:0] = "FIDO_2_0"
) are treated as text strings and
u8
slices without sentinel terminator as byte strings.
You can deserialize CBOR data into Zig objects using the parse()
function.
const e = [5]u8{ 1, 2, 3, 4, 5 };
const di = DataItem.new("\x85\x01\x02\x03\x04\x05");
const x = try parse([5]u8, di, .{});
try std.testing.expectEqualSlices(u8, e[0..], x[0..]);
You can pass options to the parse
function to influence its behaviour.
This includes:
allocator
- The allocator to be used (if necessary)duplicate_field_behavior
- How to handle duplicate fields (.UseFirst
,.Error
)ignore_unknown_fields
- Ignore unknown fields
You can also dynamically create CBOR data using the Builder
.
const allocator = std.testing.allocator;
var b = try Builder.withType(allocator, .Map);
try b.pushTextString("a");
try b.pushInt(1);
try b.pushTextString("b");
try b.enter(.Array);
try b.pushInt(2);
try b.pushInt(3);
//try b.leave(); <-- you can leave out the return at the end
const x = try b.finish();
defer allocator.free(x);
// { "a": 1, "b": [2, 3] }
try std.testing.expectEqualSlices(u8, "\xa2\x61\x61\x01\x61\x62\x82\x02\x03", x);
- The
push*
functions append a data item - The
enter
function takes a container type and pushes it on the builder stack - The
leave
function leaves the current container. The container is appended to the wrapping container - The
finish
function returns the CBOR data as owned slice
You can override the stringify
function for structs and tagged unions by implementing cborStringify
.
const Foo = struct {
x: u32 = 1234,
y: struct {
a: []const u8 = "public-key",
b: u64 = 0x1122334455667788,
},
pub fn cborStringify(self: *const @This(), options: StringifyOptions, out: anytype) !void {
// First stringify the 'y' struct
const allocator = std.testing.allocator;
var o = std.ArrayList(u8).init(allocator);
defer o.deinit();
try stringify(self.y, options, o.writer());
// Then use the Builder to alter the CBOR output
var b = try build.Builder.withType(allocator, .Map);
try b.pushTextString("x");
try b.pushInt(self.x);
try b.pushTextString("y");
try b.pushByteString(o.items);
const x = try b.finish();
defer allocator.free(x);
try out.writeAll(x);
}
};
The StringifyOptions
can be used to indirectly pass an Allocator
to the function.