- Zig 100%
| data | Added function for accessing map values using a key. | |
| examples | make the example use juicy main for io | |
| src | Merge branch 'chpill-replace-array-mult-pr' | |
| .gitignore | bumped supported version to Zig 0.14.0 (to be released soon) | |
| build.zig | migrate to zig 0.16.0 | |
| build.zig.zon | new patch release | |
| CITATION.cff | from_callback renamed to ignore_override. This fixes #25 . Thanks to Ben Krieger for helping to improve this project. | |
| license | Create license | |
| README.md | added fetch command to table | |
zbor - Zig CBOR
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 the Client to Authenticator Protocol CTAP2 which is a essential part of FIDO2 authenticators/ Passkeys.
I have utilized this library in several projects throughout the previous year, primarily in conjunction with my FIDO2 library. I'd consider it stable.
With the introduction of Zig version 0.11.0, this library will remain aligned with the most recent stable release. If you have any problems or want
to share some ideas feel free to open an issue or write me a mail, but please be kind.
Getting started
Versions
| Zig version | zbor version | fetch command |
|---|---|---|
| 0.13.0 | 0.15 | |
| 0.14.x | 0.16.x, 0.17.x, 0.18.x | |
| 0.15.x | 0.19.0, 0.20.0 | |
| 0.16.x | 0.21.x | zig fetch --save https://codeberg.org/r4gus/zbor/archive/0.21.0.tar.gz |
First add this library as a dependency to your build.zig.zon file:
# Replace <VERSION TAG> with the version you want to use
zig fetch --save https://codeberg.org/r4gus/zbor/archive/<VERSION TAG>.tar.gz
# e.g. zig fetch --save https://codeberg.org/r4gus/zbor/archive/0.20.1.tar.gz
then within you build.zig add the following code:
// First fetch the dependency...constzbor_dep=b.dependency("zbor",.{.target=target,.optimize=optimize,});constzbor_module=zbor_dep.module("zbor");// If you have a module that has zbor as a dependency...constyour_module=b.addModule("your-module",.{.root_source_file=.{.path="src/main.zig"},.imports=&.{.{.name="zbor",.module=zbor_module},},});// Or as a dependency for a executable...exe.root_module.addImport("zbor",zbor_module);Usage
This library lets you inspect and parse CBOR data without having to allocate additional memory.
Inspect CBOR data
To inspect CBOR data you must first create a new DataItem.
constcbor=@import("zbor");constdi=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.
You can also read from a std.Io.Reader using the DataItem.readAlloc function. The function will read a CBOR data item from a Reader and output a DataItem object. If you use the readAlloc method, don't forget to free the allocated memory using the `deinit() ́ method!
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?i65string- returns?[]const u8array- returns?ArrayIteratormap- returns?MapIteratorsimple- returns?u8float- returns?f64tagged- returns?Tagboolean- returns?bool
Iterators
The functions array and map will return an iterator. Every time you
call next() you will either get a DataItem/ Pair or null.
constdi=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");variter=di.array().?;while(iter.next())|value|{_=value;// doe something}Encoding and decoding
Serialization
You can serialize Zig objects into CBOR using the stringify() function.
constallocator=std.testing.allocator;varstr=std.Io.Writer.Allocating.init(allocator);deferstr.deinit();constInfo=struct{versions:[]const[]constu8,};consti=Info{.versions=&.{"FIDO_2_0"},};trystringify(i,.{},&str.writer);Note: Compile time floats are always encoded as single precision floats (f32). Please use
@floatCastbefore passing a float tostringify().
The stringify() function is convenient but also adds extra overhead. If you want full control
over the serialization process you can use the following functions defined in zbor.build: writeInt,
writeByteString, writeTextString, writeTag, writeSimple, writeArray, writeMap. For more
details check out the manual serialization example and the
corresponding source code.
Stringify Options
You can pass options to the stringify function to influence its behavior. Without passing any
options, stringify will behave as follows:
- Enums will be serialized to their textual representation
u8slices will be serialized to byte strings- For structs and unions:
nullfields are skipped by default- fields of type
std.mem.Allocatorare always skipped. - the names of fields are serialized to text strings
You can modify that behavior by changing the default options, e.g.:
constEcdsaP256Key=struct{/// kty:kty:u8=2,/// alg:alg:i8=-7,/// crv:crv:u8=1,/// x-coordinatex:[32]u8,/// y-coordinatey:[32]u8,pubfnnew(k:EcdsaP256.PublicKey)@This(){constxy=k.toUncompressedSec1();return.{.x=xy[1..33].*,.y=xy[33..65].*,};}};//...trystringify(k,.{.field_settings=&.{.{.name="kty",.field_options=.{.alias="1",.serialization_type=.Integer}},.{.name="alg",.field_options=.{.alias="3",.serialization_type=.Integer}},.{.name="crv",.field_options=.{.alias="-1",.serialization_type=.Integer}},.{.name="x",.field_options=.{.alias="-2",.serialization_type=.Integer}},.{.name="y",.field_options=.{.alias="-3",.serialization_type=.Integer}},}},&str.writer);Here we define a alias for every field of the struct and tell serialize that it should treat
those aliases as integers instead of text strings.
See Options and FieldSettings in src/parse.zig for all available options!
Deserialization
You can deserialize CBOR data into Zig objects using the parse() function.
conste=[5]u8{1,2,3,4,5};constdi=DataItem.new("\x85\x01\x02\x03\x04\x05");constx=tryparse([5]u8,di,.{});trystd.testing.expectEqualSlices(u8,e[0..],x[0..]);Parse Options
You can pass options to the parse function to influence its behaviour.
This includes:
allocator- The allocator to be used. This is required if your data type has any pointers, slices, etc.duplicate_field_behavior- How to handle duplicate fields (.UseFirst,.Error)..UseFirst- Use the first field..Error- Return an error if there are multiple fields with the same name.
ignore_unknown_fields- Ignore unknown fields (default istrue).field_settings- Lets you specify aliases for struct fields. Examples on how to usefield_settingscan be found in the examples directory and within defined tests.ignore_override- Flag to break infinity loops. This has to be set totrueif you override the behavior usingcborParseorcborStringify.
Builder
You can also dynamically create CBOR data using the Builder.
constallocator=std.testing.allocator;varb=tryBuilder.withType(allocator,.Map);tryb.pushTextString("a");tryb.pushInt(1);tryb.pushTextString("b");tryb.enter(.Array);tryb.pushInt(2);tryb.pushInt(3);//try b.leave(); <-- you can leave out the return at the endconstx=tryb.finish();deferallocator.free(x);// { "a": 1, "b": [2, 3] }trystd.testing.expectEqualSlices(u8,"\xa2\x61\x61\x01\x61\x62\x82\x02\x03",x);Commands
- The
push*functions append a data item - The
enterfunction takes a container type and pushes it on the builder stack - The
leavefunction leaves the current container. The container is appended to the wrapping container - The
finishfunction returns the CBOR data as owned slice
Overriding stringify
You can override the stringify function for structs and tagged unions by implementing cborStringify.
constFoo=struct{x:u32=1234,y:struct{a:[]constu8="public-key",b:u64=0x1122334455667788,},pubfncborStringify(self:*const@This(),options:Options,out:*std.Io.Writer)!void{// First stringify the 'y' structconstallocator=std.testing.allocator;varo=std.Io.Writer.Allocating.init(allocator);defero.deinit();trystringify(self.y,options,&o.writer);// Then use the Builder to alter the CBOR outputvarb=trybuild.Builder.withType(allocator,.Map);tryb.pushTextString("x");tryb.pushInt(self.x);tryb.pushTextString("y");tryb.pushByteString(o.written());constx=tryb.finish();deferallocator.free(x);tryout.writeAll(x);}};The StringifyOptions can be used to indirectly pass an Allocator to the function.
Please make sure to set ignore_override to true when calling recursively into stringify(self) to prevent infinite loops.
Overriding parse
You can override the parse function for structs and tagged unions by implementing cborParse. This is helpful if you have aliases for your struct members.
constEcdsaP256Key=struct{/// kty:kty:u8=2,/// alg:alg:i8=-7,/// crv:crv:u8=1,/// x-coordinatex:[32]u8,/// y-coordinatey:[32]u8,pubfncborParse(item:DataItem,options:Options)!@This(){_=options;returntryparse(@This(),item,.{.ignore_override=true,// prevent infinite loops.field_settings=&.{.{.name="kty",.field_options=.{.alias="1"}},.{.name="alg",.field_options=.{.alias="3"}},.{.name="crv",.field_options=.{.alias="-1"}},.{.name="x",.field_options=.{.alias="-2"}},.{.name="y",.field_options=.{.alias="-3"}},},});}};The Options can be used to indirectly pass an Allocator to the function.
Please make sure to set ignore_override to true when calling recursively into parse(self) to prevent infinite loops.
Structs with fields of type std.mem.Allocator
If you have a struct with a field of type std.mem.Allocator you have to override the stringify
funcation for that struct, e.g.:
pubfncborStringify(self:*const@This(),options:cbor.StringifyOptions,out:*std.Io.Writer)!void{_=options;trycbor.stringify(self,.{.ignore_override=true,.field_settings=&.{.{.name="allocator",.options=.{.skip=true}},},},out);}When using parse make sure you pass a allocator to the function. The passed allocator will be assigned
to the field of type std.mem.Allocator.
Indefinite-length Data Items
CBOR supports the serialization of many container types in two formats, definite and indefinite. For definite-length data items, the length is directly encoded into the data-items header. In contrast, indefinite-length data items are terminated by a break-byte 0xff.
Zbor currently supports indefinite-length encoding for both arrays and maps. The default serialization type for both types remains definite to support backwards compatibility. One can control the serialization type for arrays and maps via the serialization options. The two fields in question are array_serialization_type and map_serialization_type.
Indefinite-length Arrays
This is an example for serializing a array as indefinite-length map:
constarray=[_]u16{500,2};vararr=std.Io.Writer.Allocating.init(allocator);deferarr.deinit();trystringify(array,.{.allocator=allocator,.array_serialization_type=.ArrayIndefinite,},&arr.writer,);For the de-serialization of indefinite-length arrays you don't have to do anything special. The parse function will automatically detect the encoding type for you.
Indefinite-length Maps
This is an example for serializing a struct as indefinite-length map:
constallocator=std.testing.allocator;constS=struct{Fun:bool,Amt:i16,};consts=S{.Fun=true,.Amt=-2,};vararr=std.Io.Writer.Allocating.init(allocator);deferarr.deinit();trystringify(s,.{.allocator=allocator,.map_serialization_type=.MapIndefinite,},&arr.writer,);For the de-serialization of indefinite-length maps you don't have to do anything special. The parse function will automatically detect the encoding type for you.
ArrayBackedSlice
This library offers a convenient function named ArrayBackedSlice, which enables you to create a wrapper for an array of any size and type. This wrapper implements the cborStringify and cborParse methods, allowing it to seamlessly replace slices (e.g., []const u8) with an array.
test"ArrayBackedSlice test"{constallocator=std.testing.allocator;constS64B=ArrayBackedSlice(64,u8,.Byte);varx=S64B{};tryx.set("\x01\x02\x03\x04");varstr=std.Io.Writer.Allocating.init(allocator);deferstr.deinit();trystringify(x,.{},&str.writer);trystd.testing.expectEqualSlices(u8,"\x44\x01\x02\x03\x04",str.written());constdi=tryDataItem.new(str.written());consty=tryparse(S64B,di,.{});trystd.testing.expectEqualSlices(u8,"\x01\x02\x03\x04",y.get());}