imoldfella / zbor

CBOR parser written in Zig

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

zbor - Zig CBOR

GitHub GitHub Workflow Status GitHub all releases

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).

Getting started

To use this library you can either add it directly as a module or use the Zig package manager to fetchi it as dependency.

Zig package manager

First add this library as dependency to your build.zig.zon file:

.{
    .name = "your-project",
    .version = 0.0.1,

    .dependencies = .{
        .zbor = .{
            .url = "https://github.com/r4gus/zbor/archive/master.tar.gz",
            .hash = "1220bfd0526e76937238e2268ea69e97de6b79744d934e4fabd98e0d6e7a8d8e4740",
        }
    },
}

Hash

To calculate the hash you can use the following script.

Note: The Zig core team might alter the hashing algorithm used, i.e., the script might not always calculate the correct result in the future.

We also specify the hash within the release notes starting with version 0.11.3-alpha.

As a module

First add the library to your project, e.g., as a submodule:

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.

// Create a new module
var zbor_module = b.createModule(.{
    .source_file = .{ .path = "libs/zbor/src/main.zig" },
});

// create your exe ...

// Add the module to your exe/ lib
exe.addModule("zbor", zbor_module);

Usage

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.

Inspect CBOR data

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.

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

Iterators

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
}

Encoding and decoding

Serialization

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 to stringify().

u8slices 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.

Stringify Options

You can pass options to the stringify function to influence its behaviour.

This includes:

  • allocator - The allocator to be used (if necessary)
  • skip_null_fields - Struct fields that are null will not be included in the CBOR map
  • slice_as_text - Convert an u8 slice into a CBOR text string
  • enum_as_text- Use the field name instead of the numerical value to represent a enum
  • field_settings - Lets you influence how stringify treats specific fileds. The settings set using field_settings override the default settings.

Deserialization

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..]);
Parse Options

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
  • field_settings - Lets you specify aliases for struct fields

Builder

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);
Commands
  • 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

Overriding stringify

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.

Please make sure to set from_cborStringify 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.

const EcdsaP256Key = struct {
    /// kty:
    kty: u8 = 2,
    /// alg:
    alg: i8 = -7,
    /// crv:
    crv: u8 = 1,
    /// x-coordinate
    x: [32]u8,
    /// y-coordinate
    y: [32]u8,

    pub fn cborParse(item: DataItem, options: ParseOptions) !@This() {
        _ = options;
        return try parse(@This(), item, .{
            .from_cborParse = true, // prevent infinite loops
            .field_settings = &.{
                .{ .name = "kty", .alias = "1" },
                .{ .name = "alg", .alias = "3" },
                .{ .name = "crv", .alias = "-1" },
                .{ .name = "x", .alias = "-2" },
                .{ .name = "y", .alias = "-3" },
            },
        });
    }
};

The ParseOptions can be used to indirectly pass an Allocator to the function.

Please make sure to set from_cborParse to true when calling recursively into parse(self) to prevent infinite loops.

About

CBOR parser written in Zig

License:MIT License


Languages

Language:Zig 100.0%