├── .gitignore ├── data └── WebAuthnCreate.dat ├── CITATION.cff ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── main.yml ├── license ├── src ├── main.zig ├── fuzz.zig ├── builder.zig ├── cose.zig ├── cbor.zig └── tests.zig ├── examples ├── manual_serialization.zig ├── automatic_serialization.zig └── automatic_serialization2.zig └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | .zig-cache 3 | zig-out 4 | gyro.lock 5 | -------------------------------------------------------------------------------- /data/WebAuthnCreate.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r4gus/zbor/HEAD/data/WebAuthnCreate.dat -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: zbor 3 | message: >- 4 | If you use this software, please cite it using the 5 | metadata from this file. 6 | type: software 7 | authors: 8 | - given-names: David Pierre 9 | family-names: Sugar 10 | orcid: "https://orcid.org/0009-0007-0056-602X" 11 | repository-code: 'https://github.com/r4gus/zbor' 12 | abstract: 'CBOR parser written in Zig' 13 | version: 0.17.0 14 | date-released: 2025-02-10 15 | keywords: 16 | - cbor 17 | - rfc8949 18 | - zig 19 | license: MIT 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "master" branch 8 | push: 9 | branches: [ "master" ] 10 | pull_request: 11 | branches: [ "master" ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | test: 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, windows-latest] 22 | runs-on: ${{matrix.os}} 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: goto-bus-stop/setup-zig@v2 26 | with: 27 | version: 0.15.1 28 | - run: zig build test 29 | lint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: goto-bus-stop/setup-zig@v2 34 | with: 35 | version: 0.15.1 36 | - run: zig fmt --check src/*.zig 37 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 David P. Sugar 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. 22 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const testing = std.testing; 4 | 5 | const cbor = @import("cbor.zig"); 6 | const parser = @import("parse.zig"); 7 | pub const cose = @import("cose.zig"); 8 | pub const builder = @import("builder.zig"); 9 | 10 | pub const Type = cbor.Type; 11 | pub const DataItem = cbor.DataItem; 12 | pub const Tag = cbor.Tag; 13 | pub const Pair = cbor.Pair; 14 | pub const MapIterator = cbor.MapIterator; 15 | pub const ArrayIterator = cbor.ArrayIterator; 16 | 17 | pub const ParseError = parser.ParseError; 18 | pub const StringifyError = parser.StringifyError; 19 | pub const Options = parser.Options; 20 | pub const SerializationType = parser.SerializationType; 21 | pub const SkipBehavior = parser.SkipBehavior; 22 | pub const FieldSettings = parser.FieldSettings; 23 | pub const parse = parser.parse; 24 | pub const stringify = parser.stringify; 25 | 26 | pub const Builder = builder.Builder; 27 | pub const ContainerType = builder.ContainerType; 28 | 29 | pub const ArrayBackedSlice = parser.ArrayBackedSlice; 30 | pub const ArrayBackedSliceType = parser.ArrayBackedSliceType; 31 | 32 | // TODO: can we somehow read this from build.zig.zon??? 33 | pub const VERSION: []const u8 = "0.15.0"; 34 | 35 | test "main tests" { 36 | _ = cbor; 37 | _ = parser; 38 | _ = cose; 39 | _ = builder; 40 | } 41 | -------------------------------------------------------------------------------- /src/fuzz.zig: -------------------------------------------------------------------------------- 1 | const zbor = @import("main.zig"); 2 | const std = @import("std"); 3 | 4 | const ROUNDS = 10_000; 5 | 6 | test "fuzz DataItem.new" { 7 | const allocator = std.testing.allocator; 8 | 9 | //const Config = struct { 10 | // vals: struct { testing: u8, production: u8 }, 11 | // uptime: u64, 12 | //}; 13 | 14 | var i: usize = 0; 15 | while (i < ROUNDS) : (i += 1) { 16 | const bytes_to_allocate = std.crypto.random.intRangeAtMost(usize, 1, 128); 17 | const mem = try allocator.alloc(u8, bytes_to_allocate); 18 | defer allocator.free(mem); 19 | std.crypto.random.bytes(mem); 20 | 21 | _ = zbor.DataItem.new(mem) catch { 22 | continue; 23 | }; 24 | } 25 | } 26 | 27 | test "fuzz parse(Config, ...)" { 28 | const allocator = std.testing.allocator; 29 | 30 | const Config = struct { 31 | vals: struct { testing: u8, production: u8 }, 32 | uptime: u64, 33 | }; 34 | 35 | var i: usize = 0; 36 | while (i < ROUNDS) : (i += 1) { 37 | const bytes_to_allocate = std.crypto.random.intRangeAtMost(usize, 1, 128); 38 | const mem = try allocator.alloc(u8, bytes_to_allocate); 39 | defer allocator.free(mem); 40 | std.crypto.random.bytes(mem); 41 | 42 | const di = zbor.DataItem{ .data = mem }; 43 | 44 | _ = zbor.parse(Config, di, .{}) catch { 45 | continue; 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/manual_serialization.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zbor = @import("zbor"); 3 | 4 | const User = struct { 5 | id: []const u8, 6 | name: []const u8, 7 | displayName: []const u8, 8 | }; 9 | 10 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 11 | const allocator = gpa.allocator(); 12 | 13 | var stdout_buffer: [1024]u8 = undefined; 14 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 15 | const stdout = &stdout_writer.interface; 16 | 17 | pub fn main() !void { 18 | const user = User{ 19 | .id = "\x01\x23\x45\x67", 20 | .name = "bob@example.com", 21 | .displayName = "Bob", 22 | }; 23 | 24 | const expected = "\xa3\x62\x69\x64\x44\x01\x23\x45\x67\x64\x6e\x61\x6d\x65\x6f\x62\x6f\x62\x40\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x63\x42\x6f\x62"; 25 | 26 | var di = std.Io.Writer.Allocating.init(allocator); 27 | defer di.deinit(); 28 | const writer = &di.writer; 29 | 30 | try zbor.builder.writeMap(writer, 3); 31 | try zbor.builder.writeTextString(writer, "id"); 32 | try zbor.builder.writeByteString(writer, user.id); 33 | try zbor.builder.writeTextString(writer, "name"); 34 | try zbor.builder.writeTextString(writer, user.name); 35 | try zbor.builder.writeTextString(writer, "displayName"); 36 | try zbor.builder.writeTextString(writer, user.displayName); 37 | 38 | try stdout.print("expected: {x}\ngot: {x}\nmatches: {any}\n", .{ 39 | expected, 40 | di.written(), 41 | std.mem.eql(u8, expected, di.written()), 42 | }); 43 | 44 | try stdout.flush(); 45 | } 46 | -------------------------------------------------------------------------------- /examples/automatic_serialization.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zbor = @import("zbor"); 3 | 4 | const User = struct { 5 | id: []const u8, 6 | name: []const u8, 7 | displayName: []const u8, 8 | 9 | pub fn cborStringify(self: *const @This(), options: zbor.Options, out: *std.Io.Writer) !void { 10 | return zbor.stringify(self, .{ 11 | .allocator = options.allocator, 12 | .ignore_override = true, 13 | .field_settings = &.{ 14 | .{ .name = "id", .value_options = .{ .slice_serialization_type = .ByteString } }, 15 | .{ .name = "name", .value_options = .{ .slice_serialization_type = .TextString } }, 16 | .{ .name = "displayName", .value_options = .{ .slice_serialization_type = .TextString } }, 17 | }, 18 | }, out); 19 | } 20 | }; 21 | 22 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 23 | const allocator = gpa.allocator(); 24 | 25 | var stdout_buffer: [1024]u8 = undefined; 26 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 27 | const stdout = &stdout_writer.interface; 28 | 29 | pub fn main() !void { 30 | const user = User{ 31 | .id = "\x01\x23\x45\x67", 32 | .name = "bob@example.com", 33 | .displayName = "Bob", 34 | }; 35 | 36 | const expected = "\xa3\x62\x69\x64\x44\x01\x23\x45\x67\x64\x6e\x61\x6d\x65\x6f\x62\x6f\x62\x40\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x63\x42\x6f\x62"; 37 | 38 | var di = std.Io.Writer.Allocating.init(allocator); 39 | defer di.deinit(); 40 | 41 | try zbor.stringify(user, .{}, &di.writer); 42 | 43 | try stdout.print("expected: {x}\ngot: {x}\nmatches: {any}\n", .{ 44 | expected, 45 | di.written(), 46 | std.mem.eql(u8, expected, di.written()), 47 | }); 48 | 49 | try stdout.flush(); 50 | } 51 | -------------------------------------------------------------------------------- /examples/automatic_serialization2.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("zbor"); 3 | 4 | pub fn main() !void { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | const allocator = gpa.allocator(); 7 | 8 | var original_msg = Message.new("stack_id", "hello", "there"); 9 | 10 | // serialize the message 11 | var bytes = std.Io.Writer.Allocating.init(allocator); 12 | defer bytes.deinit(); 13 | 14 | original_msg.headers.token = "my cool client token that totally is awesome"; 15 | try original_msg.cborStringify(.{}, &bytes.writer); 16 | 17 | const expected = "\xa5\x00\x68\x73\x74\x61\x63\x6b\x5f\x69\x64\x01\x00\x02\x65\x68\x65\x6c\x6c\x6f\x03\x65\x74\x68\x65\x72\x65\x05\xa1\x00\x78\x2c\x6d\x79\x20\x63\x6f\x6f\x6c\x20\x63\x6c\x69\x65\x6e\x74\x20\x74\x6f\x6b\x65\x6e\x20\x74\x68\x61\x74\x20\x74\x6f\x74\x61\x6c\x6c\x79\x20\x69\x73\x20\x61\x77\x65\x73\x6f\x6d\x65"; 18 | if (!std.mem.eql(u8, expected, bytes.written())) { 19 | std.log.err("serialization failure! expected '{x}' but got '{x}'", .{ 20 | expected, 21 | bytes.written(), 22 | }); 23 | } 24 | 25 | const di: cbor.DataItem = try cbor.DataItem.new(bytes.written()); 26 | const parsed_msg = try Message.cborParse(di, .{ .allocator = allocator }); 27 | 28 | std.debug.print("msg {any}\n", .{parsed_msg}); 29 | } 30 | 31 | pub const MessageType = enum(u8) { 32 | Undefined, 33 | }; 34 | 35 | pub const Message = struct { 36 | const Self = @This(); 37 | 38 | id: []const u8, 39 | message_type: u8, 40 | topic: []const u8, 41 | content: ?[]const u8 = null, 42 | tx_id: ?[]const u8 = null, 43 | headers: Headers, 44 | 45 | // return a stack Message 46 | pub fn new(id: []const u8, topic: []const u8, content: []const u8) Message { 47 | return Message{ 48 | .id = id, 49 | .topic = topic, 50 | .message_type = @intFromEnum(MessageType.Undefined), 51 | .content = content, 52 | .tx_id = null, 53 | .headers = Headers.new(null), 54 | }; 55 | } 56 | 57 | // return a heap Message 58 | pub fn create(allocator: std.mem.Allocator, id: []const u8, topic: []const u8, content: []const u8) !*Message { 59 | const ptr = try allocator.create(Message); 60 | ptr.* = Message.new(id, topic, content); 61 | 62 | return ptr; 63 | } 64 | 65 | pub fn cborStringify(self: Self, o: cbor.Options, out: *std.Io.Writer) !void { 66 | try cbor.stringify(self, .{ 67 | .ignore_override = true, 68 | .field_settings = &.{ 69 | .{ 70 | .name = "id", // the name of the affected struct field 71 | .field_options = .{ .alias = "0", .serialization_type = .Integer }, // replace "id" with "0" and treat "0" as an integer 72 | .value_options = .{ .slice_serialization_type = .TextString }, // serialize the value of "id" as text string (major type 3) 73 | }, 74 | .{ 75 | .name = "message_type", 76 | .field_options = .{ .alias = "1", .serialization_type = .Integer }, 77 | }, 78 | .{ 79 | .name = "topic", 80 | .field_options = .{ .alias = "2", .serialization_type = .Integer }, 81 | .value_options = .{ .slice_serialization_type = .TextString }, 82 | }, 83 | .{ 84 | .name = "content", 85 | .field_options = .{ .alias = "3", .serialization_type = .Integer }, 86 | .value_options = .{ .slice_serialization_type = .TextString }, 87 | }, 88 | .{ 89 | .name = "tx_id", 90 | .field_options = .{ .alias = "4", .serialization_type = .Integer }, 91 | .value_options = .{ .slice_serialization_type = .TextString }, 92 | }, 93 | .{ 94 | .name = "headers", 95 | .field_options = .{ .alias = "5", .serialization_type = .Integer }, 96 | }, 97 | }, 98 | .allocator = o.allocator, 99 | }, out); 100 | } 101 | 102 | pub fn cborParse(item: cbor.DataItem, o: cbor.Options) !Self { 103 | return try cbor.parse(Self, item, .{ 104 | .ignore_override = true, // prevent infinite loops 105 | .field_settings = &.{ 106 | .{ 107 | .name = "id", // the name of the affected struct field 108 | .field_options = .{ .alias = "0", .serialization_type = .Integer }, // replace "id" with "0" and treat "0" as an integer 109 | .value_options = .{ .slice_serialization_type = .TextString }, // serialize the value of "id" as text string (major type 3) 110 | }, 111 | .{ 112 | .name = "message_type", 113 | .field_options = .{ .alias = "1", .serialization_type = .Integer }, 114 | }, 115 | .{ 116 | .name = "topic", 117 | .field_options = .{ .alias = "2", .serialization_type = .Integer }, 118 | .value_options = .{ .slice_serialization_type = .TextString }, 119 | }, 120 | .{ 121 | .name = "content", 122 | .field_options = .{ .alias = "3", .serialization_type = .Integer }, 123 | .value_options = .{ .slice_serialization_type = .TextString }, 124 | }, 125 | .{ 126 | .name = "tx_id", 127 | .field_options = .{ .alias = "4", .serialization_type = .Integer }, 128 | .value_options = .{ .slice_serialization_type = .TextString }, 129 | }, 130 | .{ 131 | .name = "headers", 132 | .field_options = .{ .alias = "5", .serialization_type = .Integer }, 133 | }, 134 | }, 135 | .allocator = o.allocator, 136 | }); 137 | } 138 | }; 139 | 140 | pub const Headers = struct { 141 | const Self = @This(); 142 | 143 | token: ?[]const u8, 144 | 145 | pub fn new(token: ?[]const u8) Self { 146 | return Headers{ 147 | .token = token, 148 | }; 149 | } 150 | 151 | pub fn create(allocator: std.mem.Allocator, token: ?[]const u8) !*Self { 152 | const ptr = try allocator.create(Headers); 153 | ptr.* = Headers.new(token); 154 | 155 | return ptr; 156 | } 157 | 158 | pub fn cborStringify(self: Self, o: cbor.Options, out: *std.Io.Writer) !void { 159 | try cbor.stringify(self, .{ 160 | .ignore_override = true, 161 | .field_settings = &.{ 162 | .{ 163 | .name = "token", 164 | .field_options = .{ .alias = "0", .serialization_type = .Integer }, 165 | .value_options = .{ .slice_serialization_type = .TextString }, 166 | }, 167 | }, 168 | .allocator = o.allocator, 169 | }, out); 170 | } 171 | 172 | pub fn cborParse(item: cbor.DataItem, o: cbor.Options) !Self { 173 | return try cbor.parse(Self, item, .{ 174 | .ignore_override = true, // prevent infinite loops 175 | .field_settings = &.{ 176 | .{ 177 | .name = "token", 178 | .field_options = .{ .alias = "0", .serialization_type = .Integer }, 179 | .value_options = .{ .slice_serialization_type = .TextString }, 180 | }, 181 | }, 182 | .allocator = o.allocator, 183 | }); 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zbor - Zig CBOR 2 | 3 | ![GitHub](https://img.shields.io/github/license/r4gus/zbor?style=flat-square) 4 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/r4gus/zbor/main.yml?style=flat-square) 5 | ![GitHub all releases](https://img.shields.io/github/downloads/r4gus/zbor/total?style=flat-square) 6 | 7 | 8 | The Concise Binary Object Representation (CBOR) is a data format whose design 9 | goals include the possibility of extremely small code size, fairly small 10 | message size, and extensibility without the need for version negotiation 11 | ([RFC8949](https://www.rfc-editor.org/rfc/rfc8949.html#abstract)). It is used 12 | in different protocols like the Client to Authenticator Protocol 13 | [CTAP2](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form) 14 | which is a essential part of FIDO2 authenticators/ Passkeys. 15 | 16 | I have utilized this library in several projects throughout the previous year, primarily in conjunction with my [FIDO2 library](https://github.com/r4gus/fido2). I'd consider it stable. 17 | With the introduction of [Zig version `0.11.0`](https://ziglang.org/download/), this library will remain aligned with the most recent stable release. If you have any problems or want 18 | to share some ideas feel free to open an issue or write me a mail, but please be kind. 19 | 20 | ## Getting started 21 | 22 | Versions 23 | | Zig version | zbor version | 24 | |:-----------:|:------------:| 25 | | 0.13.0 | 0.15 | 26 | | 0.14.x | 0.16.x, 0.17.x, 0.18.x | 27 | | 0.15.x | 0.19.0, 0.20.0 | 28 | 29 | First add this library as a dependency to your `build.zig.zon` file: 30 | 31 | ```bash 32 | # Replace with the version you want to use 33 | zig fetch --save https://github.com/r4gus/zbor/archive/refs/tags/.tar.gz 34 | ``` 35 | 36 | then within you `build.zig` add the following code: 37 | 38 | ```zig 39 | // First fetch the dependency... 40 | const zbor_dep = b.dependency("zbor", .{ 41 | .target = target, 42 | .optimize = optimize, 43 | }); 44 | const zbor_module = zbor_dep.module("zbor"); 45 | 46 | // If you have a module that has zbor as a dependency... 47 | const your_module = b.addModule("your-module", .{ 48 | .root_source_file = .{ .path = "src/main.zig" }, 49 | .imports = &.{ 50 | .{ .name = "zbor", .module = zbor_module }, 51 | }, 52 | }); 53 | 54 | // Or as a dependency for a executable... 55 | exe.root_module.addImport("zbor", zbor_module); 56 | ``` 57 | 58 | ## Usage 59 | 60 | This library lets you inspect and parse CBOR data without having to allocate 61 | additional memory. 62 | 63 | ### Inspect CBOR data 64 | 65 | To inspect CBOR data you must first create a new `DataItem`. 66 | 67 | ```zig 68 | const cbor = @import("zbor"); 69 | 70 | const di = DataItem.new("\x1b\xff\xff\xff\xff\xff\xff\xff\xff") catch { 71 | // handle the case that the given data is malformed 72 | }; 73 | ``` 74 | 75 | `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. 76 | 77 | To check the type of the given `DataItem` use the `getType()` function. 78 | 79 | ```zig 80 | std.debug.assert(di.getType() == .Int); 81 | ``` 82 | 83 | 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). 84 | 85 | Based on the given type you can the access the underlying value. 86 | 87 | ```zig 88 | std.debug.assert(di.int().? == 18446744073709551615); 89 | ``` 90 | 91 | 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().?`. 92 | 93 | The following getter functions are supported: 94 | * `int` - returns `?i65` 95 | * `string` - returns `?[]const u8` 96 | * `array` - returns `?ArrayIterator` 97 | * `map` - returns `?MapIterator` 98 | * `simple` - returns `?u8` 99 | * `float` - returns `?f64` 100 | * `tagged` - returns `?Tag` 101 | * `boolean` - returns `?bool` 102 | 103 | #### Iterators 104 | 105 | The functions `array` and `map` will return an iterator. Every time you 106 | call `next()` you will either get a `DataItem`/ `Pair` or `null`. 107 | 108 | ```zig 109 | 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"); 110 | 111 | var iter = di.array().?; 112 | while (iter.next()) |value| { 113 | _ = value; 114 | // doe something 115 | } 116 | ``` 117 | 118 | ### Encoding and decoding 119 | 120 | #### Serialization 121 | 122 | You can serialize Zig objects into CBOR using the `stringify()` function. 123 | 124 | ```zig 125 | const allocator = std.testing.allocator; 126 | var str = std.Io.Writer.Allocating.init(allocator); 127 | defer str.deinit(); 128 | 129 | const Info = struct { 130 | versions: []const []const u8, 131 | }; 132 | 133 | const i = Info{ 134 | .versions = &.{"FIDO_2_0"}, 135 | }; 136 | 137 | try stringify(i, .{}, &str.writer); 138 | ``` 139 | 140 | > Note: Compile time floats are always encoded as single precision floats (f32). Please use `@floatCast` 141 | > before passing a float to `stringify()`. 142 | 143 | The `stringify()` function is convenient but also adds extra overhead. If you want full control 144 | over the serialization process you can use the following functions defined in `zbor.build`: `writeInt`, 145 | `writeByteString`, `writeTextString`, `writeTag`, `writeSimple`, `writeArray`, `writeMap`. For more 146 | details check out the [manual serialization example](examples/manual_serialization.zig) and the 147 | corresponding [source code](src/builder.zig). 148 | 149 | ##### Stringify Options 150 | 151 | You can pass options to the `stringify` function to influence its behavior. Without passing any 152 | options, `stringify` will behave as follows: 153 | 154 | * Enums will be serialized to their textual representation 155 | * `u8` slices will be serialized to byte strings 156 | * For structs and unions: 157 | * `null` fields are skipped by default 158 | * fields of type `std.mem.Allocator` are always skipped. 159 | * the names of fields are serialized to text strings 160 | 161 | You can modify that behavior by changing the default options, e.g.: 162 | 163 | ```zig 164 | const EcdsaP256Key = struct { 165 | /// kty: 166 | kty: u8 = 2, 167 | /// alg: 168 | alg: i8 = -7, 169 | /// crv: 170 | crv: u8 = 1, 171 | /// x-coordinate 172 | x: [32]u8, 173 | /// y-coordinate 174 | y: [32]u8, 175 | 176 | pub fn new(k: EcdsaP256.PublicKey) @This() { 177 | const xy = k.toUncompressedSec1(); 178 | return .{ 179 | .x = xy[1..33].*, 180 | .y = xy[33..65].*, 181 | }; 182 | } 183 | }; 184 | 185 | //... 186 | 187 | try stringify(k, .{ .field_settings = &.{ 188 | .{ .name = "kty", .field_options = .{ .alias = "1", .serialization_type = .Integer } }, 189 | .{ .name = "alg", .field_options = .{ .alias = "3", .serialization_type = .Integer } }, 190 | .{ .name = "crv", .field_options = .{ .alias = "-1", .serialization_type = .Integer } }, 191 | .{ .name = "x", .field_options = .{ .alias = "-2", .serialization_type = .Integer } }, 192 | .{ .name = "y", .field_options = .{ .alias = "-3", .serialization_type = .Integer } }, 193 | } }, &str.writer); 194 | ``` 195 | 196 | Here we define a alias for every field of the struct and tell `serialize` that it should treat 197 | those aliases as integers instead of text strings. 198 | 199 | __See `Options` and `FieldSettings` in `src/parse.zig` for all available options!__ 200 | 201 | #### Deserialization 202 | 203 | You can deserialize CBOR data into Zig objects using the `parse()` function. 204 | 205 | ```zig 206 | const e = [5]u8{ 1, 2, 3, 4, 5 }; 207 | const di = DataItem.new("\x85\x01\x02\x03\x04\x05"); 208 | 209 | const x = try parse([5]u8, di, .{}); 210 | 211 | try std.testing.expectEqualSlices(u8, e[0..], x[0..]); 212 | ``` 213 | 214 | ##### Parse Options 215 | 216 | You can pass options to the `parse` function to influence its behaviour. 217 | 218 | This includes: 219 | 220 | * `allocator` - The allocator to be used. This is required if your data type has any pointers, slices, etc. 221 | * `duplicate_field_behavior` - How to handle duplicate fields (`.UseFirst`, `.Error`). 222 | * `.UseFirst` - Use the first field. 223 | * `.Error` - Return an error if there are multiple fields with the same name. 224 | * `ignore_unknown_fields` - Ignore unknown fields (default is `true`). 225 | * `field_settings` - Lets you specify aliases for struct fields. Examples on how to use `field_settings` can be found in the _examples_ directory and within defined tests. 226 | * `ignore_override` - Flag to break infinity loops. This has to be set to `true` if you override the behavior using `cborParse` or `cborStringify`. 227 | 228 | #### Builder 229 | 230 | You can also dynamically create CBOR data using the `Builder`. 231 | 232 | ```zig 233 | const allocator = std.testing.allocator; 234 | 235 | var b = try Builder.withType(allocator, .Map); 236 | try b.pushTextString("a"); 237 | try b.pushInt(1); 238 | try b.pushTextString("b"); 239 | try b.enter(.Array); 240 | try b.pushInt(2); 241 | try b.pushInt(3); 242 | //try b.leave(); <-- you can leave out the return at the end 243 | const x = try b.finish(); 244 | defer allocator.free(x); 245 | 246 | // { "a": 1, "b": [2, 3] } 247 | try std.testing.expectEqualSlices(u8, "\xa2\x61\x61\x01\x61\x62\x82\x02\x03", x); 248 | ``` 249 | 250 | ##### Commands 251 | 252 | - The `push*` functions append a data item 253 | - The `enter` function takes a container type and pushes it on the builder stack 254 | - The `leave` function leaves the current container. The container is appended to the wrapping container 255 | - The `finish` function returns the CBOR data as owned slice 256 | 257 | #### Overriding stringify 258 | 259 | You can override the `stringify` function for structs and tagged unions by implementing `cborStringify`. 260 | 261 | ```zig 262 | const Foo = struct { 263 | x: u32 = 1234, 264 | y: struct { 265 | a: []const u8 = "public-key", 266 | b: u64 = 0x1122334455667788, 267 | }, 268 | 269 | pub fn cborStringify(self: *const @This(), options: Options, out: *std.Io.Writer) !void { 270 | 271 | // First stringify the 'y' struct 272 | const allocator = std.testing.allocator; 273 | var o = std.Io.Writer.Allocating.init(allocator); 274 | defer o.deinit(); 275 | try stringify(self.y, options, &o.writer); 276 | 277 | // Then use the Builder to alter the CBOR output 278 | var b = try build.Builder.withType(allocator, .Map); 279 | try b.pushTextString("x"); 280 | try b.pushInt(self.x); 281 | try b.pushTextString("y"); 282 | try b.pushByteString(o.written()); 283 | const x = try b.finish(); 284 | defer allocator.free(x); 285 | 286 | try out.writeAll(x); 287 | } 288 | }; 289 | ``` 290 | 291 | The `StringifyOptions` can be used to indirectly pass an `Allocator` to the function. 292 | 293 | Please make sure to set `ignore_override` to `true` when calling recursively into `stringify(self)` to prevent infinite loops. 294 | 295 | #### Overriding parse 296 | 297 | 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. 298 | 299 | ```zig 300 | const EcdsaP256Key = struct { 301 | /// kty: 302 | kty: u8 = 2, 303 | /// alg: 304 | alg: i8 = -7, 305 | /// crv: 306 | crv: u8 = 1, 307 | /// x-coordinate 308 | x: [32]u8, 309 | /// y-coordinate 310 | y: [32]u8, 311 | 312 | pub fn cborParse(item: DataItem, options: Options) !@This() { 313 | _ = options; 314 | return try parse(@This(), item, .{ 315 | .ignore_override = true, // prevent infinite loops 316 | .field_settings = &.{ 317 | .{ .name = "kty", .field_options = .{ .alias = "1" } }, 318 | .{ .name = "alg", .field_options = .{ .alias = "3" } }, 319 | .{ .name = "crv", .field_options = .{ .alias = "-1" } }, 320 | .{ .name = "x", .field_options = .{ .alias = "-2" } }, 321 | .{ .name = "y", .field_options = .{ .alias = "-3" } }, 322 | }, 323 | }); 324 | } 325 | }; 326 | ``` 327 | 328 | The `Options` can be used to indirectly pass an `Allocator` to the function. 329 | 330 | Please make sure to set `ignore_override` to `true` when calling recursively into `parse(self)` to prevent infinite loops. 331 | 332 | #### Structs with fields of type `std.mem.Allocator` 333 | 334 | If you have a struct with a field of type `std.mem.Allocator` you have to override the `stringify` 335 | funcation for that struct, e.g.: 336 | 337 | ```zig 338 | pub fn cborStringify(self: *const @This(), options: cbor.StringifyOptions, out: *std.Io.Writer) !void { 339 | _ = options; 340 | 341 | try cbor.stringify(self, .{ 342 | .ignore_override = true, 343 | .field_settings = &.{ 344 | .{ .name = "allocator", .options = .{ .skip = true } }, 345 | }, 346 | }, out); 347 | } 348 | ``` 349 | 350 | When using `parse` make sure you pass a allocator to the function. The passed allocator will be assigned 351 | to the field of type `std.mem.Allocator`. 352 | 353 | #### Indefinite-length Data Items 354 | 355 | 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`. 356 | 357 | 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`. 358 | 359 | ##### Indefinite-length Arrays 360 | 361 | This is an example for serializing a array as indefinite-length map: 362 | ```zig 363 | const array = [_]u16{ 500, 2 }; 364 | 365 | var arr = std.Io.Writer.Allocating.init(allocator); 366 | defer arr.deinit(); 367 | 368 | try stringify( 369 | array, 370 | .{ 371 | .allocator = allocator, 372 | .array_serialization_type = .ArrayIndefinite, 373 | }, 374 | &arr.writer, 375 | ); 376 | ``` 377 | 378 | 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. 379 | 380 | ##### Indefinite-length Maps 381 | 382 | This is an example for serializing a struct as indefinite-length map: 383 | ```zig 384 | const allocator = std.testing.allocator; 385 | 386 | const S = struct { 387 | Fun: bool, 388 | Amt: i16, 389 | }; 390 | 391 | const s = S{ 392 | .Fun = true, 393 | .Amt = -2, 394 | }; 395 | 396 | var arr = std.Io.Writer.Allocating.init(allocator); 397 | defer arr.deinit(); 398 | 399 | try stringify( 400 | s, 401 | .{ 402 | .allocator = allocator, 403 | .map_serialization_type = .MapIndefinite, 404 | }, 405 | &arr.writer, 406 | ); 407 | ``` 408 | 409 | 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. 410 | 411 | ### ArrayBackedSlice 412 | 413 | 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. 414 | 415 | ```zig 416 | test "ArrayBackedSlice test" { 417 | const allocator = std.testing.allocator; 418 | 419 | const S64B = ArrayBackedSlice(64, u8, .Byte); 420 | var x = S64B{}; 421 | try x.set("\x01\x02\x03\x04"); 422 | 423 | var str = std.Io.Writer.Allocating.init(allocator); 424 | defer str.deinit(); 425 | 426 | try stringify(x, .{}, &str.writer); 427 | try std.testing.expectEqualSlices(u8, "\x44\x01\x02\x03\x04", str.written()); 428 | 429 | const di = try DataItem.new(str.written()); 430 | const y = try parse(S64B, di, .{}); 431 | 432 | try std.testing.expectEqualSlices(u8, "\x01\x02\x03\x04", y.get()); 433 | } 434 | ``` 435 | -------------------------------------------------------------------------------- /src/builder.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const cbor = @import("cbor.zig"); 3 | 4 | /// Serialize an integer into a CBOR integer (major type 0 or 1). 5 | pub fn writeInt(writer: *std.Io.Writer, value: i65) !void { 6 | const h: u8 = if (value < 0) 0x20 else 0; 7 | const v: u64 = @as(u64, @intCast(if (value < 0) -(value + 1) else value)); 8 | try encode(writer, h, v); 9 | } 10 | 11 | /// Serialize a slice to a CBOR byte string (major type 2). 12 | pub fn writeByteString(writer: *std.Io.Writer, value: []const u8) !void { 13 | const h: u8 = 0x40; 14 | const v: u64 = @as(u64, @intCast(value.len)); 15 | try encode(writer, h, v); 16 | try writer.writeAll(value); 17 | } 18 | 19 | /// Serialize a slice to a CBOR text string (major type 3). 20 | pub fn writeTextString(writer: *std.Io.Writer, value: []const u8) !void { 21 | const h: u8 = 0x60; 22 | const v: u64 = @as(u64, @intCast(value.len)); 23 | try encode(writer, h, v); 24 | try writer.writeAll(value); 25 | } 26 | 27 | /// Serialize a tag. 28 | /// 29 | /// You MUST serialize another data item right after calling this function. 30 | pub fn writeTag(writer: *std.Io.Writer, tag: u64) !void { 31 | const h: u8 = 0xc0; 32 | const v: u64 = tag; 33 | try encode(writer, h, v); 34 | } 35 | 36 | /// Serialize a simple value. 37 | pub fn writeSimple(writer: *std.Io.Writer, simple: u8) !void { 38 | if (24 <= simple and simple <= 31) return error.ReservedValue; 39 | const h: u8 = 0xe0; 40 | const v: u64 = @as(u64, @intCast(simple)); 41 | try encode(writer, h, v); 42 | } 43 | 44 | pub fn writeTrue(writer: *std.Io.Writer) !void { 45 | try writeSimple(writer, 21); 46 | } 47 | 48 | pub fn writeFalse(writer: *std.Io.Writer) !void { 49 | try writeSimple(writer, 20); 50 | } 51 | 52 | pub fn writeFloat(writer: *std.Io.Writer, f: anytype) !void { 53 | const T = @TypeOf(f); 54 | const TInf = @typeInfo(T); 55 | 56 | switch (TInf) { 57 | .float => |float| { 58 | switch (float.bits) { 59 | 16 => try cbor.encode_2(writer, 0xe0, @as(u64, @intCast(@as(u16, @bitCast(f))))), 60 | 32 => try cbor.encode_4(writer, 0xe0, @as(u64, @intCast(@as(u32, @bitCast(f))))), 61 | 64 => try cbor.encode_8(writer, 0xe0, @as(u64, @intCast(@as(u64, @bitCast(f))))), 62 | else => @compileError("Float must be 16, 32 or 64 Bits wide"), 63 | } 64 | }, 65 | else => return error.NotAFloat, 66 | } 67 | } 68 | 69 | /// Write the header of an array to `writer`. 70 | /// 71 | /// You must write exactly `len` data items to `writer` afterwards. 72 | pub inline fn writeArray(writer: *std.Io.Writer, len: u64) !void { 73 | try encode(writer, 0x80, len); 74 | } 75 | 76 | /// Write the header of a map to `writer`. 77 | /// 78 | /// You must write exactly `len` key-value pairs (data items) to `writer` afterwards. 79 | pub inline fn writeMap(writer: *std.Io.Writer, len: u64) !void { 80 | try encode(writer, 0xa0, len); 81 | } 82 | 83 | /// Type of a Builder container 84 | pub const ContainerType = enum { 85 | Root, 86 | Array, 87 | Map, 88 | }; 89 | 90 | const Entry = struct { 91 | t: ContainerType = .Root, 92 | cnt: u64 = 0, 93 | raw: std.Io.Writer.Allocating, 94 | 95 | pub fn new(allocator: std.mem.Allocator, t: ContainerType) @This() { 96 | return .{ 97 | .t = t, 98 | .cnt = 0, 99 | .raw = .init(allocator), 100 | }; 101 | } 102 | }; 103 | 104 | /// A Builder lets you dynamically generate CBOR data. 105 | pub const Builder = struct { 106 | stack: std.ArrayListUnmanaged(Entry), 107 | allocator: std.mem.Allocator, 108 | 109 | /// Create a new builder. 110 | /// 111 | /// On error all allocated memory is freed. 112 | pub fn new(allocator: std.mem.Allocator) !@This() { 113 | return withType(allocator, .Root); 114 | } 115 | 116 | /// Create a new builder with the given container type. 117 | /// 118 | /// On error all allocated memory is freed. 119 | pub fn withType(allocator: std.mem.Allocator, t: ContainerType) !@This() { 120 | var b = @This(){ 121 | .stack = .{}, 122 | .allocator = allocator, 123 | }; 124 | 125 | // The stack has at least one element on it: the Root 126 | b.stack.append(b.allocator, Entry.new(allocator, .Root)) catch |e| { 127 | b.unwind(); 128 | return e; 129 | }; 130 | // If we want to use a container type just push another 131 | // entry onto the stack. The container will later be 132 | // merged into the root 133 | if (t != .Root) { 134 | b.stack.append(b.allocator, Entry.new(allocator, t)) catch |e| { 135 | b.unwind(); 136 | return e; 137 | }; 138 | } 139 | return b; 140 | } 141 | 142 | /// Serialize an integer. 143 | /// 144 | /// On error all allocated memory is freed. After this 145 | /// point one MUST NOT access the builder! 146 | pub fn pushInt(self: *@This(), value: i65) !void { 147 | writeInt(&self.top().raw.writer, value) catch |e| { 148 | self.unwind(); 149 | return e; 150 | }; 151 | self.top().cnt += 1; 152 | } 153 | 154 | /// Serialize a slice as byte string. 155 | /// 156 | /// On error all allocated memory is freed. After this 157 | /// point one MUST NOT access the builder! 158 | pub fn pushByteString(self: *@This(), value: []const u8) !void { 159 | writeByteString(&self.top().raw.writer, value) catch |e| { 160 | self.unwind(); 161 | return e; 162 | }; 163 | self.top().cnt += 1; 164 | } 165 | 166 | /// Serialize a slice as text string. 167 | /// 168 | /// On error all allocated memory is freed. After this 169 | /// point one MUST NOT access the builder! 170 | pub fn pushTextString(self: *@This(), value: []const u8) !void { 171 | writeTextString(&self.top().raw.writer, value) catch |e| { 172 | self.unwind(); 173 | return e; 174 | }; 175 | self.top().cnt += 1; 176 | } 177 | 178 | /// Serialize a tag. 179 | /// 180 | /// You MUST serialize another data item right after calling this function. 181 | /// 182 | /// On error all allocated memory is freed. After this 183 | /// point one MUST NOT access the builder! 184 | pub fn pushTag(self: *@This(), tag: u64) !void { 185 | writeTag(&self.top().raw.writer, tag) catch |e| { 186 | self.unwind(); 187 | return e; 188 | }; 189 | } 190 | 191 | /// Serialize a simple value. 192 | /// 193 | /// On error (except for ReservedValue) all allocated memory is freed. 194 | /// After this point one MUST NOT access the builder! 195 | pub fn pushSimple(self: *@This(), simple: u8) !void { 196 | writeSimple(&self.top().raw.writer, simple) catch |e| { 197 | self.unwind(); 198 | return e; 199 | }; 200 | } 201 | 202 | /// Add a chunk of CBOR. 203 | /// 204 | /// The given CBOR data is only added if its well formed. 205 | /// 206 | /// On error (except for MalformedCbor) all allocated memory is freed. 207 | /// After this point one MUST NOT access the builder! 208 | pub fn pushCbor(self: *@This(), input: []const u8) !void { 209 | // First check that the given cbor is well formed 210 | var i: usize = 0; 211 | if (!cbor.validate(input, &i, true)) return error.MalformedCbor; 212 | 213 | // Append the cbor data 214 | self.top().raw.writer.writeAll(input) catch |e| { 215 | self.unwind(); 216 | return e; 217 | }; 218 | self.top().cnt += 1; 219 | } 220 | 221 | /// Enter a data structure (Array or Map) 222 | /// 223 | /// On error (except for InvalidContainerType) all allocated memory is freed. 224 | /// After this point one MUST NOT access the builder! 225 | pub fn enter(self: *@This(), t: ContainerType) !void { 226 | if (t == .Root) return error.InvalidContainerType; 227 | 228 | self.stack.append(self.allocator, Entry.new(self.allocator, t)) catch |e| { 229 | self.unwind(); 230 | return e; 231 | }; 232 | } 233 | 234 | /// Leave the current data structure 235 | /// 236 | /// On error (except for EmptyStack and InvalidPairCount) all allocated 237 | /// memory is freed. After this point one MUST NOT access the builder! 238 | pub fn leave(self: *@This()) !void { 239 | if (self.stack.items.len < 2) return error.EmptyStack; 240 | if (self.top().t == .Map and self.top().cnt & 0x01 != 0) 241 | return error.InvalidPairCount; 242 | 243 | try self.moveUp(); 244 | } 245 | 246 | /// Return the serialized data. 247 | /// 248 | /// The caller is responsible for freeing the data. 249 | /// 250 | /// On error (except for InvalidPairCount) all allocated 251 | /// memory is freed. After this point one MUST NOT access the builder! 252 | pub fn finish(self: *@This()) ![]u8 { 253 | if (self.top().t == .Map and self.top().cnt & 0x01 != 0) 254 | return error.InvalidPairCount; 255 | 256 | // unwind the stack if neccessary 257 | while (self.stack.items.len > 1) { 258 | try self.moveUp(); 259 | } 260 | 261 | const s = self.stack.items[0].raw.toOwnedSlice(); 262 | self.stack.deinit(self.allocator); 263 | return s; 264 | } 265 | 266 | fn moveUp(self: *@This()) !void { 267 | var e = self.stack.pop().?; 268 | defer e.raw.deinit(); 269 | 270 | switch (e.t) { 271 | .Array => writeArray(&self.top().raw.writer, e.cnt) catch |err| { 272 | self.unwind(); 273 | return err; 274 | }, 275 | .Map => writeMap(&self.top().raw.writer, e.cnt / 2) catch |err| { 276 | self.unwind(); 277 | return err; 278 | }, 279 | .Root => unreachable, 280 | } 281 | self.top().raw.writer.writeAll(e.raw.written()) catch |err| { 282 | self.unwind(); 283 | return err; 284 | }; 285 | self.top().cnt += 1; 286 | } 287 | 288 | /// Return a mutable reference to the element at the top of the stack. 289 | fn top(self: *@This()) *Entry { 290 | return &self.stack.items[self.stack.items.len - 1]; 291 | } 292 | 293 | /// Free all allocated memory on error. This is meant 294 | /// to prevent memory leaks if the builder throws an error. 295 | fn unwind(self: *@This()) void { 296 | while (self.stack.items.len > 0) { 297 | var e = self.stack.pop().?; 298 | e.raw.deinit(); 299 | } 300 | self.stack.deinit(self.allocator); 301 | } 302 | }; 303 | 304 | fn encode(out: *std.Io.Writer, head: u8, v: u64) !void { 305 | switch (v) { 306 | 0x00...0x17 => { 307 | try out.writeByte(head | @as(u8, @intCast(v))); 308 | }, 309 | 0x18...0xff => { 310 | try out.writeByte(head | 24); 311 | try out.writeByte(@as(u8, @intCast(v))); 312 | }, 313 | 0x0100...0xffff => try cbor.encode_2(out, head, v), 314 | 0x00010000...0xffffffff => try cbor.encode_4(out, head, v), 315 | 0x0000000100000000...0xffffffffffffffff => try cbor.encode_8(out, head, v), 316 | } 317 | } 318 | 319 | fn testInt(expected: []const u8, i: i65) !void { 320 | const allocator = std.testing.allocator; 321 | 322 | var b = try Builder.new(allocator); 323 | try b.pushInt(i); 324 | const x = try b.finish(); 325 | defer allocator.free(x); 326 | try std.testing.expectEqualSlices(u8, expected, x); 327 | } 328 | 329 | fn testByteString(expected: []const u8, i: []const u8) !void { 330 | const allocator = std.testing.allocator; 331 | 332 | var b = try Builder.new(allocator); 333 | try b.pushByteString(i); 334 | const x = try b.finish(); 335 | defer allocator.free(x); 336 | try std.testing.expectEqualSlices(u8, expected, x); 337 | } 338 | 339 | fn testTextString(expected: []const u8, i: []const u8) !void { 340 | const allocator = std.testing.allocator; 341 | 342 | var b = try Builder.new(allocator); 343 | try b.pushTextString(i); 344 | const x = try b.finish(); 345 | defer allocator.free(x); 346 | try std.testing.expectEqualSlices(u8, expected, x); 347 | } 348 | 349 | test "stringify int with builder" { 350 | try testInt("\x1b\x00\x00\x00\x02\xdf\xdc\x1c\x34", 12345678900); 351 | try testInt("\x18\x7b", 123); 352 | 353 | try testInt("\x3a\x00\x0f\x3d\xdc", -998877); 354 | try testInt("\x3b\xff\xff\xff\xff\xff\xff\xff\xff", -18446744073709551616); 355 | } 356 | 357 | test "stringify string with builder" { 358 | try testByteString("\x45\x10\x11\x12\x13\x14", "\x10\x11\x12\x13\x14"); 359 | 360 | try testTextString("\x64\x49\x45\x54\x46", "IETF"); 361 | try testTextString("\x62\x22\x5c", "\"\\"); 362 | } 363 | 364 | test "stringify array using builder 1" { 365 | const allocator = std.testing.allocator; 366 | var b = try Builder.withType(allocator, .Array); 367 | try b.pushInt(1); 368 | try b.enter(.Array); // array 1 start 369 | try b.pushInt(2); 370 | try b.pushInt(3); 371 | try b.leave(); // array 1 end 372 | try b.enter(.Array); // array 2 start 373 | try b.pushInt(4); 374 | try b.pushInt(5); 375 | try b.leave(); // array 2 end 376 | const x = try b.finish(); 377 | defer allocator.free(x); 378 | 379 | try std.testing.expectEqualSlices(u8, "\x83\x01\x82\x02\x03\x82\x04\x05", x); 380 | } 381 | 382 | test "stringify array using builder 2" { 383 | const allocator = std.testing.allocator; 384 | var b = try Builder.withType(allocator, .Array); 385 | var i: i65 = 1; 386 | while (i < 26) : (i += 1) { 387 | try b.pushInt(i); 388 | } 389 | const x = try b.finish(); 390 | defer allocator.free(x); 391 | 392 | try std.testing.expectEqualSlices(u8, "\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", x); 393 | } 394 | 395 | test "stringify map using builder 1" { 396 | const allocator = std.testing.allocator; 397 | var b = try Builder.withType(allocator, .Map); 398 | try b.pushInt(1); 399 | try b.pushInt(2); 400 | try b.pushInt(3); 401 | try b.pushInt(4); 402 | const x = try b.finish(); 403 | defer allocator.free(x); 404 | 405 | try std.testing.expectEqualSlices(u8, "\xa2\x01\x02\x03\x04", x); 406 | } 407 | 408 | test "stringify nested map using builder 1" { 409 | const allocator = std.testing.allocator; 410 | var b = try Builder.withType(allocator, .Map); 411 | try b.pushTextString("a"); 412 | try b.pushInt(1); 413 | try b.pushTextString("b"); 414 | try b.enter(.Array); 415 | try b.pushInt(2); 416 | try b.pushInt(3); 417 | //try b.leave(); <-- you can leave out the return at the end 418 | const x = try b.finish(); 419 | defer allocator.free(x); 420 | 421 | try std.testing.expectEqualSlices(u8, "\xa2\x61\x61\x01\x61\x62\x82\x02\x03", x); 422 | } 423 | 424 | test "stringify nested array using builder 1" { 425 | const allocator = std.testing.allocator; 426 | var b = try Builder.withType(allocator, .Array); 427 | try b.pushTextString("a"); 428 | try b.enter(.Map); 429 | try b.pushTextString("b"); 430 | try b.pushTextString("c"); 431 | try b.leave(); 432 | const x = try b.finish(); 433 | defer allocator.free(x); 434 | 435 | try std.testing.expectEqualSlices(u8, "\x82\x61\x61\xa1\x61\x62\x61\x63", x); 436 | } 437 | 438 | test "stringify tag using builder 1" { 439 | const allocator = std.testing.allocator; 440 | var b = try Builder.new(allocator); 441 | try b.pushTag(0); 442 | try b.pushTextString("2013-03-21T20:04:00Z"); 443 | const x = try b.finish(); 444 | defer allocator.free(x); 445 | 446 | try std.testing.expectEqualSlices(u8, "\xc0\x74\x32\x30\x31\x33\x2d\x30\x33\x2d\x32\x31\x54\x32\x30\x3a\x30\x34\x3a\x30\x30\x5a", x); 447 | } 448 | 449 | test "stringify simple using builder 1" { 450 | const allocator = std.testing.allocator; 451 | var b = try Builder.new(allocator); 452 | try b.pushSimple(255); 453 | const x = try b.finish(); 454 | defer allocator.free(x); 455 | 456 | try std.testing.expectEqualSlices(u8, "\xf8\xff", x); 457 | } 458 | 459 | test "write true false" { 460 | const allocator = std.testing.allocator; 461 | 462 | var arr = std.Io.Writer.Allocating.init(allocator); 463 | defer arr.deinit(); 464 | 465 | try writeTrue(&arr.writer); 466 | try writeFalse(&arr.writer); 467 | 468 | try std.testing.expectEqual(@as(u8, 0xf5), arr.written()[0]); 469 | try std.testing.expectEqual(@as(u8, 0xf4), arr.written()[1]); 470 | } 471 | 472 | test "write float #1" { 473 | const allocator = std.testing.allocator; 474 | var arr = std.Io.Writer.Allocating.init(allocator); 475 | defer arr.deinit(); 476 | 477 | try writeFloat(&arr.writer, @as(f16, @floatCast(0.0))); 478 | 479 | try std.testing.expectEqualSlices(u8, "\xf9\x00\x00", arr.written()); 480 | } 481 | 482 | test "write float #2" { 483 | const allocator = std.testing.allocator; 484 | var arr = std.Io.Writer.Allocating.init(allocator); 485 | defer arr.deinit(); 486 | 487 | try writeFloat(&arr.writer, @as(f16, @floatCast(-0.0))); 488 | 489 | try std.testing.expectEqualSlices(u8, "\xf9\x80\x00", arr.written()); 490 | } 491 | 492 | test "write float #3" { 493 | const allocator = std.testing.allocator; 494 | var arr = std.Io.Writer.Allocating.init(allocator); 495 | defer arr.deinit(); 496 | 497 | try writeFloat(&arr.writer, @as(f32, @floatCast(3.4028234663852886e+38))); 498 | 499 | try std.testing.expectEqualSlices(u8, "\xfa\x7f\x7f\xff\xff", arr.written()); 500 | } 501 | 502 | test "write float #4" { 503 | const allocator = std.testing.allocator; 504 | var arr = std.Io.Writer.Allocating.init(allocator); 505 | defer arr.deinit(); 506 | 507 | try writeFloat(&arr.writer, @as(f64, @floatCast(-4.1))); 508 | 509 | try std.testing.expectEqualSlices(u8, "\xfb\xc0\x10\x66\x66\x66\x66\x66\x66", arr.written()); 510 | } 511 | -------------------------------------------------------------------------------- /src/cose.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const cbor = @import("cbor.zig"); 4 | const Type = cbor.Type; 5 | const DataItem = cbor.DataItem; 6 | const Tag = cbor.Tag; 7 | const Pair = cbor.Pair; 8 | const MapIterator = cbor.MapIterator; 9 | const ArrayIterator = cbor.ArrayIterator; 10 | const parse_ = @import("parse.zig"); 11 | const stringify = parse_.stringify; 12 | const parse = parse_.parse; 13 | const Options = parse_.Options; 14 | 15 | const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256; 16 | 17 | /// COSE algorithm identifiers 18 | pub const Algorithm = enum(i32) { 19 | /// RSASSA-PKCS1-v1_5 using SHA-1 20 | Rs1 = -65535, 21 | /// WalnutDSA signature 22 | WalnutDSA = -260, 23 | /// RSASSA-PKCS1-v1_5 using SHA-512 24 | Rs512 = -259, 25 | /// RSASSA-PKCS1-v1_5 using SHA-384 26 | Rs384 = -258, 27 | /// RSASSA-PKCS1-v1_5 using SHA-256 28 | Rs256 = -257, 29 | /// ECDSA using secp256k1 curve and SHA-256 30 | ES256K = -47, 31 | /// HSS/LMS hash-based digital signature 32 | HssLms = -46, 33 | /// SHAKE-256 512-bit Hash Value 34 | Shake256 = -45, 35 | /// SHA-2 512-bit Hash 36 | Sha512 = -44, 37 | /// SHA-2 384-bit Hash 38 | Sha384 = -43, 39 | /// RSAES-OAEP w/ SHA-512 40 | RsaesOaepSha512 = -42, 41 | /// RSAES-OAEP w/ SHA-256 42 | RsaesOaepSha256 = -41, 43 | /// RSAES-OAEP w/ SHA-1 44 | RsaesOaepDefault = -40, 45 | /// RSASSA-PSS w/ SHA-512 46 | Ps512 = -39, 47 | /// RSASSA-PSS w/ SHA-384 48 | Ps384 = -38, 49 | /// RSASSA-PSS w/ SHA-256 50 | Ps256 = -37, 51 | /// ECDSA w/ SHA-512 52 | Es512 = -36, 53 | /// ECDSA w/ SHA-384 54 | Es384 = -35, 55 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key 56 | EcdhSsA256Kw = -34, 57 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key 58 | EcdhSsA192Kw = -33, 59 | /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key 60 | EcdhSsA128Kw = -32, 61 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key 62 | EcdhEsA256Kw = -31, 63 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key 64 | EcdhEsA192Kw = -30, 65 | /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key 66 | EcdhEsA128Kw = -29, 67 | /// ECDH SS w/ HKDF - generate key directly 68 | EcdhSsHkdf512 = -28, 69 | /// ECDH SS w/ HKDF - generate key directly 70 | EcdhSsHkdf256 = -27, 71 | /// ECDH ES w/ HKDF - generate key directly 72 | EcdhEsHkdf512 = -26, 73 | /// ECDH ES w/ HKDF - generate key directly 74 | EcdhEsHkdf256 = -25, 75 | /// SHAKE-128 256-bit Hash Value 76 | Shake128 = -18, 77 | /// SHA-2 512-bit Hash truncated to 256-bits 78 | Sha512_256 = -17, 79 | /// SHA-2 256-bit Hash 80 | Sha256 = -16, 81 | /// SHA-2 256-bit Hash truncated to 64-bits 82 | Sha256_64 = -15, 83 | /// SHA-1 Hash 84 | Sha1 = -14, 85 | /// Shared secret w/ AES-MAC 256-bit key 86 | DirectHkdfAes256 = -13, 87 | /// Shared secret w/ AES-MAC 128-bit key 88 | DirectHkdfAes128 = -12, 89 | /// Shared secret w/ HKDF and SHA-512 90 | DirectHkdfSha512 = -11, 91 | /// Shared secret w/ HKDF and SHA-256 92 | DirectHkdfSha256 = -10, 93 | /// EdDSA 94 | EdDsa = -8, 95 | /// ECDSA w/ SHA-256 96 | Es256 = -7, 97 | /// Direct use of CEK 98 | Direct = -6, 99 | /// AES Key Wrap w/ 256-bit key 100 | A256Kw = -5, 101 | /// AES Key Wrap w/ 192-bit key 102 | A192Kw = -4, 103 | /// AES Key Wrap w/ 128-bit key 104 | A128Kw = -3, 105 | /// AES-GCM mode w/ 128-bit key, 128-bit tag 106 | A128Gcm = 1, 107 | /// AES-GCM mode w/ 192-bit key, 128-bit tag 108 | A192Gcm = 2, 109 | /// AES-GCM mode w/ 256-bit key, 128-bit tag 110 | A256Gcm = 3, 111 | 112 | pub fn to_raw(self: @This()) [4]u8 { 113 | const i = @intFromEnum(self); 114 | return std.mem.asBytes(&i).*; 115 | } 116 | 117 | pub fn from_raw(raw: [4]u8) @This() { 118 | return @as(@This(), @enumFromInt(std.mem.bytesToValue(i32, &raw))); 119 | } 120 | }; 121 | 122 | /// COSE key types 123 | pub const KeyType = enum(u8) { 124 | /// Octet Key Pair 125 | Okp = 1, 126 | /// Elliptic Curve Keys w/ x- and y-coordinate pair 127 | Ec2 = 2, 128 | /// RSA Key 129 | Rsa = 3, 130 | /// Symmetric Keys 131 | Symmetric = 4, 132 | /// Public key for HSS/LMS hash-based digital signature 133 | HssLms = 5, 134 | /// WalnutDSA public key 135 | WalnutDsa = 6, 136 | }; 137 | 138 | /// COSE elliptic curves 139 | pub const Curve = enum(i16) { 140 | /// NIST P-256 also known as secp256r1 (EC2) 141 | P256 = 1, 142 | /// NIST P-384 also known as secp384r1 (EC2) 143 | P384 = 2, 144 | /// NIST P-521 also known as secp521r1 (EC2) 145 | P521 = 3, 146 | /// X25519 for use w/ ECDH only (OKP) 147 | X25519 = 4, 148 | /// X448 for use w/ ECDH only (OKP) 149 | X448 = 5, 150 | /// Ed25519 for use w/ EdDSA only (OKP) 151 | Ed25519 = 6, 152 | /// Ed448 for use w/ EdDSA only (OKP) 153 | Ed448 = 7, 154 | /// SECG secp256k1 curve (EC2) 155 | secp256k1 = 8, 156 | 157 | /// Return the `KeyType` of the given elliptic curve 158 | pub fn keyType(self: @This()) KeyType { 159 | return switch (self) { 160 | .P256, .P384, .P521, .secp256k1 => .Ec2, 161 | else => .Okp, 162 | }; 163 | } 164 | }; 165 | 166 | pub const KeyTag = enum { P256 }; 167 | 168 | pub const Key = union(KeyTag) { 169 | P256: struct { 170 | /// kty: Identification of the key type 171 | kty: KeyType = .Ec2, 172 | /// alg: Key usage restriction to this algorithm 173 | alg: Algorithm, 174 | /// crv: EC identifier -- Taken from the "COSE Elliptic Curves" registry 175 | crv: Curve = .P256, 176 | /// x: x-coordinate 177 | x: [32]u8, 178 | /// y: y-coordinate 179 | y: [32]u8, 180 | /// Private key 181 | d: ?[32]u8 = null, 182 | }, 183 | 184 | pub fn getAlg(self: *const @This()) Algorithm { 185 | switch (self.*) { 186 | .P256 => |k| return k.alg, 187 | } 188 | } 189 | 190 | pub fn getPrivKey(self: *const @This()) []const u8 { 191 | switch (self.*) { 192 | .P256 => |k| { 193 | return if (k.d) |d| d[0..] else null; 194 | }, 195 | } 196 | } 197 | 198 | pub fn copySecure(self: *const @This()) @This() { 199 | switch (self.*) { 200 | .P256 => |k| { 201 | return .{ .P256 = .{ 202 | .kty = k.kty, 203 | .alg = k.alg, 204 | .crv = k.crv, 205 | .x = k.x, 206 | .y = k.y, 207 | .d = null, 208 | } }; 209 | }, 210 | } 211 | } 212 | 213 | pub fn fromP256Pub(alg: Algorithm, pk: anytype) @This() { 214 | const sec1 = pk.toUncompressedSec1(); 215 | return .{ .P256 = .{ 216 | .alg = alg, 217 | .x = sec1[1..33].*, 218 | .y = sec1[33..65].*, 219 | } }; 220 | } 221 | 222 | pub fn fromP256PrivPub(alg: Algorithm, privk: anytype, pubk: anytype) @This() { 223 | const sec1 = pubk.toUncompressedSec1(); 224 | const pk = privk.toBytes(); 225 | return .{ .P256 = .{ 226 | .alg = alg, 227 | .x = sec1[1..33].*, 228 | .y = sec1[33..65].*, 229 | .d = pk, 230 | } }; 231 | } 232 | 233 | /// Creates a new ECDSA P-256 (secp256r1) key pair for the ES256 algorithm. 234 | /// 235 | /// - `seed`: Optional seed to derive the key pair from. If `null`, a random seed will be used. 236 | /// 237 | /// Returns the newly created key pair as a structure containing the algorithm identifier, 238 | /// public key coordinates, and the secret key. 239 | /// 240 | /// # Examples 241 | /// 242 | /// ```zig 243 | /// const cbor = @import("zbor"); 244 | /// const keyPair = try cbor.cose.Key.es256(null); 245 | /// 246 | /// // Use the key pair... 247 | /// ``` 248 | pub fn es256(seed: ?[32]u8) !@This() { 249 | const kp = if (seed) |seed_| 250 | try EcdsaP256Sha256.KeyPair.generateDeterministic(seed_) 251 | else 252 | EcdsaP256Sha256.KeyPair.generate(); 253 | const sec1 = kp.public_key.toUncompressedSec1(); 254 | const pk = kp.secret_key.toBytes(); 255 | return .{ .P256 = .{ 256 | .alg = .Es256, 257 | .x = sec1[1..33].*, 258 | .y = sec1[33..65].*, 259 | .d = pk, 260 | } }; 261 | } 262 | 263 | /// Signs the provided data using the specified algorithm and key. 264 | /// 265 | /// - `data_seq`: A sequence of data slices to be signed together. 266 | /// - `allocator`: Allocator to allocate memory for the signature. 267 | /// 268 | /// Returns the DER-encoded signature as a dynamically allocated byte slice, 269 | /// or an error if the algorithm or key is unsupported. 270 | /// 271 | /// The user is responsible for freeing the allocated memory. 272 | /// 273 | /// # Errors 274 | /// 275 | /// - `error.UnsupportedAlgorithm`: If the algorithm is not supported. 276 | /// 277 | /// # Examples 278 | /// 279 | /// ```zig 280 | /// const result = try key.sign(&.{data}, allocator); 281 | /// 282 | /// // Use the signature... 283 | /// ``` 284 | pub fn sign( 285 | self: *const @This(), 286 | data_seq: []const []const u8, 287 | allocator: std.mem.Allocator, 288 | ) ![]const u8 { 289 | switch (self.*) { 290 | .P256 => |k| { 291 | if (k.d == null) return error.MissingPrivateKey; 292 | 293 | switch (k.alg) { 294 | .Es256 => { 295 | var kp = try EcdsaP256Sha256.KeyPair.fromSecretKey( 296 | try EcdsaP256Sha256.SecretKey.fromBytes(k.d.?), 297 | ); 298 | var signer = try kp.signer(null); 299 | 300 | // Append data that should be signed together 301 | for (data_seq) |data| { 302 | signer.update(data); 303 | } 304 | 305 | // Sign the data 306 | const sig = try signer.finalize(); 307 | var buffer: [EcdsaP256Sha256.Signature.der_encoded_length_max]u8 = undefined; 308 | const der = sig.toDer(&buffer); 309 | const mem = try allocator.alloc(u8, der.len); 310 | @memcpy(mem, der); 311 | return mem; 312 | }, 313 | else => return error.UnsupportedAlgorithm, 314 | } 315 | }, 316 | } 317 | } 318 | 319 | /// Verifies a signature using the provided public key and a data sequence. 320 | /// 321 | /// - `signature`: signature to be verified. 322 | /// - `data_seq`: Array of data slices that were signed together. 323 | /// 324 | /// Returns `true` if the signature is valid, `false` otherwise. 325 | /// 326 | /// # Examples 327 | /// 328 | /// ```zig 329 | /// const signatureValid = try key.verify(signature, &.{data}); 330 | /// if (signatureValid) { 331 | /// // Signature is valid 332 | /// } else { 333 | /// // Signature is not valid 334 | /// } 335 | /// ``` 336 | pub fn verify( 337 | self: *const @This(), 338 | signature: []const u8, 339 | data_seq: []const []const u8, 340 | ) !bool { 341 | switch (self.*) { 342 | .P256 => |k| { 343 | switch (k.alg) { 344 | .Es256 => { 345 | // Get public key struct 346 | var usec1: [65]u8 = undefined; 347 | usec1[0] = 4; 348 | @memcpy(usec1[1..33], &k.x); 349 | @memcpy(usec1[33..65], &k.y); 350 | const pk = try EcdsaP256Sha256.PublicKey.fromSec1(&usec1); 351 | // Get signature struct 352 | const sig = try EcdsaP256Sha256.Signature.fromDer(signature); 353 | // Get verifier 354 | var verifier = try sig.verifier(pk); 355 | for (data_seq) |data| { 356 | verifier.update(data); 357 | } 358 | verifier.verify() catch { 359 | // Verification failed 360 | return false; 361 | }; 362 | 363 | return true; 364 | }, 365 | else => return error.UnsupportedAlgorithm, 366 | } 367 | }, 368 | } 369 | } 370 | 371 | pub fn cborStringify(self: *const @This(), options: Options, out: anytype) !void { 372 | _ = options; 373 | return stringify(self, .{ 374 | .ignore_override = true, 375 | .field_settings = &.{ 376 | .{ 377 | .name = "kty", 378 | .field_options = .{ 379 | .alias = "1", 380 | .serialization_type = .Integer, 381 | }, 382 | .value_options = .{ .enum_serialization_type = .Integer }, 383 | }, 384 | .{ 385 | .name = "alg", 386 | .field_options = .{ 387 | .alias = "3", 388 | .serialization_type = .Integer, 389 | }, 390 | .value_options = .{ .enum_serialization_type = .Integer }, 391 | }, 392 | .{ 393 | .name = "crv", 394 | .field_options = .{ 395 | .alias = "-1", 396 | .serialization_type = .Integer, 397 | }, 398 | .value_options = .{ .enum_serialization_type = .Integer }, 399 | }, 400 | .{ .name = "x", .field_options = .{ 401 | .alias = "-2", 402 | .serialization_type = .Integer, 403 | } }, 404 | .{ .name = "y", .field_options = .{ 405 | .alias = "-3", 406 | .serialization_type = .Integer, 407 | } }, 408 | .{ .name = "d", .field_options = .{ 409 | .alias = "-4", 410 | .serialization_type = .Integer, 411 | } }, 412 | }, 413 | }, out); 414 | } 415 | 416 | pub fn cborParse(item: cbor.DataItem, options: Options) !@This() { 417 | return try parse(@This(), item, .{ 418 | .allocator = options.allocator, 419 | .ignore_override = true, // prevent infinite loops 420 | .field_settings = &.{ 421 | .{ 422 | .name = "kty", 423 | .field_options = .{ 424 | .alias = "1", 425 | .serialization_type = .Integer, 426 | }, 427 | }, 428 | .{ 429 | .name = "alg", 430 | .field_options = .{ 431 | .alias = "3", 432 | .serialization_type = .Integer, 433 | }, 434 | }, 435 | .{ 436 | .name = "crv", 437 | .field_options = .{ 438 | .alias = "-1", 439 | .serialization_type = .Integer, 440 | }, 441 | }, 442 | .{ .name = "x", .field_options = .{ 443 | .alias = "-2", 444 | .serialization_type = .Integer, 445 | } }, 446 | .{ .name = "y", .field_options = .{ 447 | .alias = "-3", 448 | .serialization_type = .Integer, 449 | } }, 450 | .{ .name = "d", .field_options = .{ 451 | .alias = "-4", 452 | .serialization_type = .Integer, 453 | } }, 454 | }, 455 | }); 456 | } 457 | }; 458 | 459 | test "cose Key p256 stringify #1" { 460 | const x = try EcdsaP256Sha256.PublicKey.fromSec1("\x04\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52"); 461 | 462 | const k = Key.fromP256Pub(.Es256, x); 463 | 464 | const allocator = std.testing.allocator; 465 | var str = std.Io.Writer.Allocating.init(allocator); 466 | defer str.deinit(); 467 | 468 | try stringify(k, .{}, &str.writer); 469 | 470 | try std.testing.expectEqualSlices(u8, "\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\x22\x58\x20\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52", str.written()); 471 | } 472 | 473 | test "cose Key p256 parse #1" { 474 | const payload = "\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e\x22\x58\x20\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52"; 475 | 476 | const di = try DataItem.new(payload); 477 | 478 | const key = try parse(Key, di, .{}); 479 | 480 | try std.testing.expectEqual(Algorithm.Es256, key.P256.alg); 481 | try std.testing.expectEqual(KeyType.Ec2, key.P256.kty); 482 | try std.testing.expectEqual(Curve.P256, key.P256.crv); 483 | try std.testing.expectEqualSlices(u8, "\xd9\xf4\xc2\xa3\x52\x13\x6f\x19\xc9\xa9\x5d\xa8\x82\x4a\xb5\xcd\xc4\xd5\x63\x1e\xbc\xfd\x5b\xdb\xb0\xbf\xff\x25\x36\x09\x12\x9e", &key.P256.x); 484 | try std.testing.expectEqualSlices(u8, "\xef\x40\x4b\x88\x07\x65\x57\x60\x07\x88\x8a\x3e\xd6\xab\xff\xb4\x25\x7b\x71\x23\x55\x33\x25\xd4\x50\x61\x3c\xb5\xbc\x9a\x3a\x52", &key.P256.y); 485 | } 486 | 487 | test "alg to raw" { 488 | const es256 = Algorithm.Es256; 489 | const x: [4]u8 = es256.to_raw(); 490 | 491 | try std.testing.expectEqualSlices(u8, "\xF9\xFF\xFF\xFF", &x); 492 | } 493 | 494 | test "raw to alg" { 495 | const x: [4]u8 = "\xF9\xFF\xFF\xFF".*; 496 | 497 | try std.testing.expectEqual(Algorithm.Es256, Algorithm.from_raw(x)); 498 | } 499 | 500 | test "es256 sign verify 1" { 501 | const allocator = std.testing.allocator; 502 | const msg = "Hello, World!"; 503 | 504 | const kp1 = EcdsaP256Sha256.KeyPair.generate(); 505 | 506 | // Create a signature via cose key struct 507 | var cosep256 = Key.fromP256PrivPub(.Es256, kp1.secret_key, kp1.public_key); 508 | const sig_der_1 = try cosep256.sign(&.{msg}, allocator); 509 | defer allocator.free(sig_der_1); 510 | 511 | // Verify the created signature 512 | const sig1 = try EcdsaP256Sha256.Signature.fromDer(sig_der_1); 513 | sig1.verify(msg, kp1.public_key) catch { 514 | try std.testing.expect(false); // expected void but got error 515 | }; 516 | 517 | // Verify the created signature again 518 | try std.testing.expectEqual(true, try cosep256.verify(sig_der_1, &.{msg})); 519 | 520 | // Create another key-pair 521 | var kp2 = try Key.es256(null); 522 | 523 | // Trying to verfiy the first signature using the new key-pair should fail 524 | try std.testing.expectEqual(false, try kp2.verify(sig_der_1, &.{msg})); 525 | } 526 | 527 | test "copy secure #1" { 528 | const kp1 = EcdsaP256Sha256.KeyPair.generate(); 529 | var cosep256 = Key.fromP256PrivPub(.Es256, kp1.secret_key, kp1.public_key); 530 | const cpy = cosep256.copySecure(); 531 | try std.testing.expectEqual(cpy.P256.d, null); 532 | } 533 | -------------------------------------------------------------------------------- /src/cbor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// CBOR type (see RFC 8949) 4 | pub const Type = enum { 5 | /// Integer in the range -2^64..2^64 - 1 (MT 0/1) 6 | Int, 7 | /// Byte string (MT 2) 8 | ByteString, 9 | /// UTF-8 text string (MT 3) 10 | TextString, 11 | /// Array of data items (MT 4) 12 | Array, 13 | /// Indefinite array of data items (MT 4) 14 | ArrayIndef, 15 | /// Map of pairs of data items (MT 5) 16 | Map, 17 | /// Indefinite map of pairs of data items (MT 5) 18 | MapIndef, 19 | /// False (MT 7) 20 | False, 21 | /// True (MT 7) 22 | True, 23 | /// Null (MT 7) 24 | Null, 25 | /// Undefined (MT 7) 26 | Undefined, 27 | /// Simple value (MT 7) 28 | Simple, 29 | /// Tagged data item whose tag number is an integer in the range 0..2^64 - 1 (MT 6) 30 | Tagged, 31 | /// Floating point value (MT 7) 32 | Float, 33 | /// Break (MT 7) 34 | Break, 35 | /// Unknown data item 36 | Unknown, 37 | 38 | /// Detect the data item type encoded within a raw byte 39 | pub fn fromByte(b: u8) @This() { 40 | return switch (b) { 41 | 0x00...0x3b => .Int, 42 | 0x40...0x5b => .ByteString, 43 | 0x60...0x7b => .TextString, 44 | 0x80...0x9b => .Array, 45 | 0x9f => .ArrayIndef, 46 | 0xa0...0xbb => .Map, 47 | 0xbf => .MapIndef, 48 | 0xf4 => .False, 49 | 0xf5 => .True, 50 | 0xf6 => .Null, 51 | 0xf7 => .Undefined, 52 | 0xe0...0xf3, 0xf8 => .Simple, 53 | 0xc0...0xdb => .Tagged, 54 | 0xf9...0xfb => .Float, 55 | 0xff => .Break, 56 | else => .Unknown, 57 | }; 58 | } 59 | }; 60 | 61 | /// DataItem is a wrapper around raw CBOR data 62 | pub const DataItem = struct { 63 | /// Raw CBOR data 64 | data: []const u8, 65 | 66 | /// Create a new DataItem from raw CBOR data 67 | /// 68 | /// This function will run a check to verify that the data is not malfromed as defined by RFC 8949 69 | /// before returning a DataItem. Returns an error if the data is malformed. 70 | pub fn new(data: []const u8) !@This() { 71 | var i: usize = 0; 72 | if (!validate(data, &i, false)) return error.Malformed; 73 | return .{ .data = data }; 74 | } 75 | 76 | /// Get the Type of the given DataItem 77 | pub fn getType(self: @This()) Type { 78 | return Type.fromByte(self.data[0]); 79 | } 80 | 81 | /// Decode the given DataItem into an integer 82 | /// 83 | /// The function will return null if the given DataItems doesn't have the 84 | /// type Type.Int. 85 | pub fn int(self: @This()) ?i65 { 86 | if (self.data[0] <= 0x1b and self.data[0] >= 0x00) { 87 | return @as(i65, @intCast(if (additionalInfo(self.data, null)) |v| v else return null)); 88 | } else if (self.data[0] <= 0x3b and self.data[0] >= 0x20) { 89 | return -@as(i65, @intCast(if (additionalInfo(self.data, null)) |v| v else return null)) - 1; 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | /// Decode the given DataItem into a string 96 | /// 97 | /// The function will return null if the given DataItems doesn't have the 98 | /// type Type.ByteString or Type.TextString. 99 | pub fn string(self: @This()) ?[]const u8 { 100 | const T = Type.fromByte(self.data[0]); 101 | if (T != Type.ByteString and T != Type.TextString) return null; 102 | 103 | var begin: usize = 0; 104 | const len = if (additionalInfo(self.data, &begin)) |v| @as(usize, @intCast(v)) else return null; 105 | 106 | return self.data[begin .. begin + len]; 107 | } 108 | 109 | /// Decode the given DataItem into a array 110 | /// 111 | /// This function will return an ArrayIterator on success and null if 112 | /// the given DataItem doesn't have the type Type.Array. 113 | pub fn array(self: @This()) ?ArrayIterator { 114 | const T = Type.fromByte(self.data[0]); 115 | if (T != Type.Array) return null; 116 | 117 | var begin: usize = 0; 118 | const len = if (additionalInfo(self.data, &begin)) |v| @as(usize, @intCast(v)) else return null; 119 | 120 | // Get to the end of the array 121 | var end: usize = 0; 122 | if (burn(self.data, &end) == null) return null; 123 | 124 | return ArrayIterator{ 125 | .data = self.data[begin..end], 126 | .len = len, 127 | .count = 0, 128 | .i = 0, 129 | }; 130 | } 131 | 132 | pub fn arrayIndef(self: @This()) ?IndefArrayIterator { 133 | const T = Type.fromByte(self.data[0]); 134 | if (T != Type.ArrayIndef) return null; 135 | 136 | return IndefArrayIterator{ 137 | .data = self.data[1..], 138 | .i = 0, 139 | }; 140 | } 141 | 142 | /// Decode the given DataItem into a map 143 | /// 144 | /// This function will return an MapIterator on success and null if 145 | /// the given DataItem doesn't have the type Type.Map. 146 | pub fn map(self: @This()) ?MapIterator { 147 | const T = Type.fromByte(self.data[0]); 148 | if (T != Type.Map) return null; 149 | 150 | var begin: usize = 0; 151 | const len = if (additionalInfo(self.data, &begin)) |v| @as(usize, @intCast(v)) else return null; 152 | 153 | // Get to the end of the map 154 | var end: usize = 0; 155 | if (burn(self.data, &end) == null) return null; 156 | 157 | return MapIterator{ 158 | .data = self.data[begin..end], 159 | .len = len, 160 | }; 161 | } 162 | 163 | /// Decode the given DataItem into an indefinite map 164 | /// 165 | /// This function will return an IndefMapIterator on success and null if 166 | /// the given DataItem doesn't have the type Type.MapIndef. 167 | pub fn mapIndef(self: @This()) ?MapIterator { 168 | const T = Type.fromByte(self.data[0]); 169 | if (T != Type.MapIndef) return null; 170 | 171 | return .{ 172 | .data = self.data[1..], 173 | .len = 0, // we don't know the actual number of expected items 174 | .indef = true, 175 | }; 176 | } 177 | 178 | /// Decode the given DataItem into a simple value 179 | /// 180 | /// This function will return null if the DataItems type 181 | /// is not Type.Simple, Type.False, Type.True, Type.Null 182 | /// or Type.Undefined. 183 | pub fn simple(self: @This()) ?u8 { 184 | return switch (self.data[0]) { 185 | 0xe0...0xf7 => self.data[0] & 0x1f, 186 | 0xf8 => self.data[1], 187 | else => null, 188 | }; 189 | } 190 | 191 | /// Decode the given DataItem into a boolean 192 | /// 193 | /// Returns null if the DataItem's type is not 194 | /// Type.False or Type.True. 195 | pub fn boolean(self: @This()) ?bool { 196 | return switch (self.data[0]) { 197 | 0xf4 => false, 198 | 0xf5 => true, 199 | else => null, 200 | }; 201 | } 202 | 203 | /// Decode the given DataItem into a float 204 | /// 205 | /// This function will return null if the DataItem 206 | /// isn't a half-, single-, or double precision 207 | /// floating point value. 208 | pub fn float(self: @This()) ?f64 { 209 | const T = Type.fromByte(self.data[0]); 210 | if (T != Type.Float) return null; 211 | 212 | if (additionalInfo(self.data, null)) |v| { 213 | return switch (self.data[0]) { 214 | 0xf9 => @as(f64, @floatCast(@as(f16, @bitCast(@as(u16, @intCast(v)))))), 215 | 0xfa => @as(f64, @floatCast(@as(f32, @bitCast(@as(u32, @intCast(v)))))), 216 | 0xfb => @as(f64, @bitCast(v)), 217 | else => unreachable, 218 | }; 219 | } else { 220 | return null; 221 | } 222 | } 223 | 224 | pub fn isFloat16(self: @This()) bool { 225 | if (self.data.len < 3) return false; 226 | return self.data[0] == 0xf9; 227 | } 228 | 229 | pub fn getFloat16(self: @This()) ?f16 { 230 | if (!self.isFloat16()) return null; 231 | if (additionalInfo(self.data, null)) |v| { 232 | return @floatCast(@as(f16, @bitCast(@as(u16, @intCast(v))))); 233 | } 234 | return null; 235 | } 236 | 237 | pub fn isFloat32(self: @This()) bool { 238 | if (self.data.len < 5) return false; 239 | return self.data[0] == 0xfa; 240 | } 241 | 242 | pub fn getFloat32(self: @This()) ?f32 { 243 | if (!self.isFloat32()) return null; 244 | if (additionalInfo(self.data, null)) |v| { 245 | return @floatCast(@as(f32, @bitCast(@as(u32, @intCast(v))))); 246 | } 247 | return null; 248 | } 249 | 250 | pub fn isFloat64(self: @This()) bool { 251 | if (self.data.len < 9) return false; 252 | return self.data[0] == 0xfb; 253 | } 254 | 255 | pub fn getFloat64(self: @This()) ?f64 { 256 | if (!self.isFloat64()) return null; 257 | if (additionalInfo(self.data, null)) |v| { 258 | return @floatCast(@as(f64, @bitCast(@as(u64, @intCast(v))))); 259 | } 260 | return null; 261 | } 262 | 263 | /// Decode the given DataItem into a Tag 264 | /// 265 | /// This function will return null if the DataItem 266 | /// isn't of type Type.Tagged. 267 | pub fn tagged(self: @This()) ?Tag { 268 | const T = Type.fromByte(self.data[0]); 269 | if (T != Type.Tagged) return null; 270 | 271 | var begin: usize = 0; 272 | const nr = if (additionalInfo(self.data, &begin)) |v| v else return null; 273 | 274 | return Tag{ 275 | .nr = nr, 276 | .content = DataItem.new(self.data[begin..]) catch { 277 | unreachable; // this can only be if DataItem hasn't been instantiated with new() 278 | }, 279 | }; 280 | } 281 | }; 282 | 283 | /// Representaion of a tagged data item 284 | pub const Tag = struct { 285 | /// The tag of the data item 286 | nr: u64, 287 | /// The data item being tagged 288 | content: DataItem, 289 | }; 290 | 291 | /// The key-value pair of a map 292 | pub const Pair = struct { 293 | key: DataItem, 294 | value: DataItem, 295 | }; 296 | 297 | /// Iterator for iterating over a map, returned by DataItem.map() 298 | pub const MapIterator = struct { 299 | data: []const u8, 300 | len: usize, 301 | count: usize = 0, 302 | i: usize = 0, 303 | indef: bool = false, 304 | 305 | /// Get the next key Pair 306 | /// 307 | /// Returns null after the last element. 308 | pub fn next(self: *@This()) ?Pair { 309 | if (self.i >= self.data.len) return null; 310 | 311 | if (self.indef) { 312 | // break marker means iterator is done 313 | const t = Type.fromByte(self.data[self.i]); 314 | if (t == Type.Break) return null; 315 | } else { 316 | if (self.count >= self.len) return null; 317 | } 318 | 319 | var new_i: usize = self.i; 320 | if (burn(self.data, &new_i) == null) return null; 321 | const k = DataItem.new(self.data[self.i..new_i]) catch { 322 | unreachable; // this can only be if DataItem hasn't been instantiated with new() 323 | }; 324 | self.i = new_i; 325 | 326 | if (burn(self.data, &new_i) == null) return null; 327 | const v = DataItem.new(self.data[self.i..new_i]) catch { 328 | unreachable; // this can only be if DataItem hasn't been instantiated with new() 329 | }; 330 | self.i = new_i; 331 | 332 | self.count += 1; 333 | return Pair{ .key = k, .value = v }; 334 | } 335 | }; 336 | 337 | /// Iterator for iterating over an array, returned by DataItem.array() 338 | pub const ArrayIterator = struct { 339 | data: []const u8, 340 | len: usize, 341 | count: usize, 342 | i: usize, 343 | 344 | /// Get the next DataItem 345 | /// 346 | /// Returns null after the last element. 347 | pub fn next(self: *@This()) ?DataItem { 348 | if (self.count >= self.len) return null; 349 | 350 | var new_i: usize = self.i; 351 | if (burn(self.data, &new_i) == null) return null; 352 | 353 | const tmp = self.data[self.i..new_i]; 354 | self.i = new_i; 355 | self.count += 1; 356 | return DataItem.new(tmp) catch { 357 | unreachable; 358 | }; 359 | } 360 | }; 361 | 362 | /// Iterator for iterating over an indefinite length array, returned by DataItem.arrayIndef() 363 | pub const IndefArrayIterator = struct { 364 | data: []const u8, 365 | i: usize, 366 | 367 | pub fn next(self: *@This()) ?DataItem { 368 | if (self.i >= self.data.len) return null; 369 | 370 | // break marker means iterator is done 371 | const T = Type.fromByte(self.data[self.i]); 372 | if (T == Type.Break) return null; 373 | 374 | var new_i: usize = self.i; 375 | if (burn(self.data, &new_i) == null) { 376 | return null; 377 | } 378 | const tmp = self.data[self.i..new_i]; 379 | self.i = new_i; 380 | 381 | return DataItem.new(tmp) catch { 382 | unreachable; 383 | }; 384 | } 385 | }; 386 | 387 | /// Move the index `i` to the beginning of the next data item. 388 | fn burn(data: []const u8, i: *usize) ?void { 389 | if (i.* >= data.len) return null; 390 | var offset: usize = 0; 391 | const len = if (additionalInfo(data[i.*..], &offset)) |v| @as(usize, @intCast(v)) else return null; 392 | 393 | switch (data[i.*]) { 394 | 0x00...0x1b => i.* += offset, 395 | 0x20...0x3b => i.* += offset, 396 | 0x40...0x5b => i.* += offset + len, 397 | 0x60...0x7b => i.* += offset + len, 398 | 0x80...0x9b => { 399 | i.* += offset; 400 | var x: usize = 0; 401 | while (x < len) : (x += 1) { 402 | if (burn(data, i) == null) { 403 | return null; 404 | } 405 | } 406 | }, 407 | 0x9f => { 408 | i.* += offset; 409 | while (data[i.*] != 0xff) { 410 | if (burn(data, i) == null) { 411 | return null; 412 | } 413 | } 414 | i.* += 1; 415 | }, 416 | 0xa0...0xbb => { 417 | i.* += offset; 418 | var x: usize = 0; 419 | while (x < len) : (x += 1) { 420 | // this is NOT redundant!!! 421 | if (burn(data, i) == null or burn(data, i) == null) { 422 | return null; 423 | } 424 | } 425 | }, 426 | 0xc0...0xdb => { 427 | i.* += offset; 428 | if (burn(data, i) == null) return null; 429 | }, 430 | 0xe0...0xfb => i.* += offset, 431 | else => return null, 432 | } 433 | 434 | if (i.* > data.len) { 435 | // this indicates an error 436 | i.* = data.len; 437 | return null; 438 | } 439 | } 440 | 441 | /// Return the additional information of the given data item. 442 | /// 443 | /// Pass a reference to `l` if you want to know where the 444 | /// actual data begins (l := |head| + |additional information|). 445 | fn additionalInfo(data: []const u8, l: ?*usize) ?u64 { 446 | if (data.len < 1) return null; 447 | 448 | switch (data[0] & 0x1f) { 449 | 0x00...0x17 => { 450 | if (l != null) l.?.* = 1; 451 | return @as(u64, @intCast(data[0] & 0x1f)); 452 | }, 453 | 0x18 => { 454 | if (data.len < 2) return null; 455 | if (l != null) l.?.* = 2; 456 | return @as(u64, @intCast(data[1])); 457 | }, 458 | 0x19 => { 459 | if (data.len < 3) return null; 460 | if (l != null) l.?.* = 3; 461 | return @as(u64, @intCast(unsigned_16(data[1..3]))); 462 | }, 463 | 0x1a => { 464 | if (data.len < 5) return null; 465 | if (l != null) l.?.* = 5; 466 | return @as(u64, @intCast(unsigned_32(data[1..5]))); 467 | }, 468 | 0x1b => { 469 | if (data.len < 9) return null; 470 | if (l != null) l.?.* = 9; 471 | return @as(u64, @intCast(unsigned_64(data[1..9]))); 472 | }, 473 | 0x1f => { 474 | if (data.len < 1) return null; 475 | if (l != null) l.?.* = 1; 476 | return 0x00; 477 | }, 478 | else => return null, 479 | } 480 | } 481 | 482 | /// Check if the given CBOR data is well formed 483 | /// 484 | /// * `data` - Raw CBOR data 485 | /// * `i` - Pointer to an index (must be initialized to 0) 486 | /// * `check_len` - It's important that `data` doesn't contain any extra bytes at the end [Yes/no] 487 | /// 488 | /// Returns true if the given data is well formed, false otherwise. 489 | pub fn validate(data: []const u8, i: *usize, check_len: bool) bool { 490 | if (i.* >= data.len) return false; 491 | const ib: u8 = data[i.*]; 492 | i.* += 1; 493 | const mt = ib >> 5; 494 | const ai = ib & 0x1f; 495 | var val: usize = @as(usize, @intCast(ai)); 496 | 497 | switch (ai) { 498 | 24, 25, 26, 27 => { 499 | const bytes = @as(usize, @intCast(1)) << @intCast(ai - 24); 500 | if (i.* + bytes > data.len) return false; 501 | val = 0; 502 | for (data[i.* .. i.* + bytes]) |byte| { 503 | val <<= 8; 504 | val += byte; 505 | } 506 | i.* += bytes; 507 | }, 508 | 28, 29, 30 => return false, 509 | 31 => { 510 | // Check if CBOR data, which we expect to be an indefinite length array, 511 | // is valid. An indefinite array is a sequence of data items that are not 512 | // tagged with a length and instead are terminated by a break marker (0xff). 513 | // The data items in the indefinite array can be of any type, including 514 | // other indefinite arrays. 515 | switch (mt) { 516 | 2, 3 => return false, // We don't support indefinite length *strings*. 517 | 4 => { 518 | while (i.* < data.len and Type.fromByte(data[i.*]) != .Break and validate(data, i, false)) {} 519 | if (i.* >= data.len or Type.fromByte(data[i.*]) != .Break) return false; 520 | i.* += 1; // we move the cursor over the Break-code 521 | }, 522 | 5 => { 523 | while (i.* < data.len and Type.fromByte(data[i.*]) != .Break and validate(data, i, false) and validate(data, i, false)) {} 524 | if (i.* >= data.len or Type.fromByte(data[i.*]) != .Break) return false; 525 | i.* += 1; // we move the cursor over the Break-code 526 | }, 527 | 7 => return false, // we encountered a "stand alone" break-code 528 | else => return false, 529 | } 530 | return true; 531 | }, 532 | else => {}, 533 | } 534 | 535 | switch (mt) { 536 | 2, 3 => i.* += val, 537 | 4 => { 538 | var j: usize = 0; 539 | while (j < val) : (j += 1) { 540 | if (!validate(data, i, false)) return false; 541 | } 542 | }, 543 | 5 => { 544 | var j: usize = 0; 545 | while (j < val) : (j += 1) { 546 | if (!validate(data, i, false)) return false; 547 | if (!validate(data, i, false)) return false; 548 | } 549 | }, 550 | 6 => if (!validate(data, i, false)) return false, 551 | 7 => if (ai == 24 and val < 32) return false, 552 | else => {}, 553 | } 554 | 555 | // no bytes must be left in the input 556 | if (check_len and i.* != data.len) return false; 557 | return true; 558 | } 559 | 560 | pub inline fn unsigned_16(data: []const u8) u16 { 561 | return @as(u16, @intCast(data[0])) << 8 | @as(u16, @intCast(data[1])); 562 | } 563 | 564 | pub inline fn unsigned_32(data: []const u8) u32 { 565 | return @as(u32, @intCast(data[0])) << 24 | @as(u32, @intCast(data[1])) << 16 | @as(u32, @intCast(data[2])) << 8 | @as(u32, @intCast(data[3])); 566 | } 567 | 568 | pub inline fn unsigned_64(data: []const u8) u64 { 569 | return @as(u64, @intCast(data[0])) << 56 | @as(u64, @intCast(data[1])) << 48 | @as(u64, @intCast(data[2])) << 40 | @as(u64, @intCast(data[3])) << 32 | @as(u64, @intCast(data[4])) << 24 | @as(u64, @intCast(data[5])) << 16 | @as(u64, @intCast(data[6])) << 8 | @as(u64, @intCast(data[7])); 570 | } 571 | 572 | pub inline fn encode_2(cbor: anytype, head: u8, v: u64) !void { 573 | try cbor.writeByte(head | 25); 574 | try cbor.writeByte(@as(u8, @intCast((v >> 8) & 0xff))); 575 | try cbor.writeByte(@as(u8, @intCast(v & 0xff))); 576 | } 577 | 578 | pub inline fn encode_4(cbor: anytype, head: u8, v: u64) !void { 579 | try cbor.writeByte(head | 26); 580 | try cbor.writeByte(@as(u8, @intCast((v >> 24) & 0xff))); 581 | try cbor.writeByte(@as(u8, @intCast((v >> 16) & 0xff))); 582 | try cbor.writeByte(@as(u8, @intCast((v >> 8) & 0xff))); 583 | try cbor.writeByte(@as(u8, @intCast(v & 0xff))); 584 | } 585 | 586 | pub inline fn encode_8(cbor: anytype, head: u8, v: u64) !void { 587 | try cbor.writeByte(head | 27); 588 | try cbor.writeByte(@as(u8, @intCast((v >> 56) & 0xff))); 589 | try cbor.writeByte(@as(u8, @intCast((v >> 48) & 0xff))); 590 | try cbor.writeByte(@as(u8, @intCast((v >> 40) & 0xff))); 591 | try cbor.writeByte(@as(u8, @intCast((v >> 32) & 0xff))); 592 | try cbor.writeByte(@as(u8, @intCast((v >> 24) & 0xff))); 593 | try cbor.writeByte(@as(u8, @intCast((v >> 16) & 0xff))); 594 | try cbor.writeByte(@as(u8, @intCast((v >> 8) & 0xff))); 595 | try cbor.writeByte(@as(u8, @intCast(v & 0xff))); 596 | } 597 | 598 | test "deserialize unsigned" { 599 | const di1 = try DataItem.new("\x00"); 600 | try std.testing.expectEqual(Type.Int, di1.getType()); 601 | try std.testing.expectEqual(di1.int().?, 0); 602 | 603 | const di2 = try DataItem.new("\x01"); 604 | try std.testing.expectEqual(Type.Int, di2.getType()); 605 | try std.testing.expectEqual(di2.int().?, 1); 606 | 607 | const di3 = try DataItem.new("\x17"); 608 | try std.testing.expectEqual(Type.Int, di3.getType()); 609 | try std.testing.expectEqual(di3.int().?, 23); 610 | 611 | const di4 = try DataItem.new("\x18\x18"); 612 | try std.testing.expectEqual(Type.Int, di4.getType()); 613 | try std.testing.expectEqual(di4.int().?, 24); 614 | 615 | const di5 = try DataItem.new("\x18\x64"); 616 | try std.testing.expectEqual(Type.Int, di5.getType()); 617 | try std.testing.expectEqual(di5.int().?, 100); 618 | 619 | const di6 = try DataItem.new("\x19\x03\xe8"); 620 | try std.testing.expectEqual(Type.Int, di6.getType()); 621 | try std.testing.expectEqual(di6.int().?, 1000); 622 | 623 | const di7 = try DataItem.new("\x1a\x00\x0f\x42\x40"); 624 | try std.testing.expectEqual(Type.Int, di7.getType()); 625 | try std.testing.expectEqual(di7.int().?, 1000000); 626 | 627 | const di8 = try DataItem.new("\x1b\x00\x00\x00\xe8\xd4\xa5\x10\x00"); 628 | try std.testing.expectEqual(Type.Int, di8.getType()); 629 | try std.testing.expectEqual(di8.int().?, 1000000000000); 630 | 631 | const di9 = try DataItem.new("\x1b\xff\xff\xff\xff\xff\xff\xff\xff"); 632 | try std.testing.expectEqual(Type.Int, di9.getType()); 633 | try std.testing.expectEqual(di9.int().?, 18446744073709551615); 634 | } 635 | 636 | test "deserialize negative" { 637 | const di1 = try DataItem.new("\x20"); 638 | try std.testing.expectEqual(Type.Int, di1.getType()); 639 | try std.testing.expectEqual(di1.int().?, -1); 640 | 641 | const di2 = try DataItem.new("\x29"); 642 | try std.testing.expectEqual(Type.Int, di2.getType()); 643 | try std.testing.expectEqual(di2.int().?, -10); 644 | 645 | const di3 = try DataItem.new("\x38\x63"); 646 | try std.testing.expectEqual(Type.Int, di3.getType()); 647 | try std.testing.expectEqual(di3.int().?, -100); 648 | 649 | const di6 = try DataItem.new("\x39\x03\xe7"); 650 | try std.testing.expectEqual(Type.Int, di6.getType()); 651 | try std.testing.expectEqual(di6.int().?, -1000); 652 | 653 | const di9 = try DataItem.new("\x3b\xff\xff\xff\xff\xff\xff\xff\xff"); 654 | try std.testing.expectEqual(Type.Int, di9.getType()); 655 | try std.testing.expectEqual(di9.int().?, -18446744073709551616); 656 | } 657 | 658 | test "deserialize byte string" { 659 | const di1 = try DataItem.new("\x40"); 660 | try std.testing.expectEqual(Type.ByteString, di1.getType()); 661 | try std.testing.expectEqualSlices(u8, di1.string().?, ""); 662 | 663 | const di2 = try DataItem.new("\x44\x01\x02\x03\x04"); 664 | try std.testing.expectEqual(Type.ByteString, di2.getType()); 665 | try std.testing.expectEqualSlices(u8, di2.string().?, "\x01\x02\x03\x04"); 666 | } 667 | 668 | test "deserialize text string" { 669 | const di1 = try DataItem.new("\x60"); 670 | try std.testing.expectEqual(Type.TextString, di1.getType()); 671 | try std.testing.expectEqualStrings(di1.string().?, ""); 672 | 673 | const di2 = try DataItem.new("\x61\x61"); 674 | try std.testing.expectEqual(Type.TextString, di2.getType()); 675 | try std.testing.expectEqualStrings(di2.string().?, "a"); 676 | 677 | const di3 = try DataItem.new("\x64\x49\x45\x54\x46"); 678 | try std.testing.expectEqual(Type.TextString, di3.getType()); 679 | try std.testing.expectEqualStrings(di3.string().?, "IETF"); 680 | 681 | const di4 = try DataItem.new("\x62\x22\x5c"); 682 | try std.testing.expectEqual(Type.TextString, di4.getType()); 683 | try std.testing.expectEqualStrings(di4.string().?, "\"\\"); 684 | 685 | const di5 = try DataItem.new("\x62\xc3\xbc"); 686 | try std.testing.expectEqual(Type.TextString, di5.getType()); 687 | try std.testing.expectEqualStrings(di5.string().?, "ü"); 688 | 689 | const di6 = try DataItem.new("\x63\xe6\xb0\xb4"); 690 | try std.testing.expectEqual(Type.TextString, di6.getType()); 691 | try std.testing.expectEqualStrings(di6.string().?, "水"); 692 | } 693 | 694 | test "deserialize array" { 695 | const di1 = try DataItem.new("\x80"); 696 | try std.testing.expectEqual(Type.Array, di1.getType()); 697 | var ai1 = di1.array().?; 698 | try std.testing.expectEqual(ai1.next(), null); 699 | try std.testing.expectEqual(ai1.next(), null); 700 | 701 | const di2 = try DataItem.new("\x83\x01\x02\x03"); 702 | try std.testing.expectEqual(Type.Array, di2.getType()); 703 | var ai2 = di2.array().?; 704 | try std.testing.expectEqual(ai2.next().?.int().?, 1); 705 | try std.testing.expectEqual(ai2.next().?.int().?, 2); 706 | try std.testing.expectEqual(ai2.next().?.int().?, 3); 707 | try std.testing.expectEqual(ai2.next(), null); 708 | 709 | const di3 = try 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"); 710 | try std.testing.expectEqual(Type.Array, di3.getType()); 711 | var ai3 = di3.array().?; 712 | var i: u64 = 1; 713 | while (i <= 25) : (i += 1) { 714 | try std.testing.expectEqual(ai3.next().?.int().?, i); 715 | } 716 | try std.testing.expectEqual(ai3.next(), null); 717 | 718 | const di4 = try DataItem.new("\x83\x01\x82\x02\x03\x82\x04\x05"); 719 | try std.testing.expectEqual(Type.Array, di4.getType()); 720 | var ai4 = di4.array().?; 721 | try std.testing.expectEqual(ai4.next().?.int().?, 1); 722 | var ai4_1 = ai4.next().?.array().?; 723 | try std.testing.expectEqual(ai4_1.next().?.int().?, 2); 724 | try std.testing.expectEqual(ai4_1.next().?.int().?, 3); 725 | try std.testing.expectEqual(ai4_1.next(), null); 726 | var ai4_2 = ai4.next().?.array().?; 727 | try std.testing.expectEqual(ai4_2.next().?.int().?, 4); 728 | try std.testing.expectEqual(ai4_2.next().?.int().?, 5); 729 | try std.testing.expectEqual(ai4_2.next(), null); 730 | try std.testing.expectEqual(ai4.next(), null); 731 | } 732 | 733 | test "deserialize map" { 734 | const di1 = try DataItem.new("\xa0"); 735 | try std.testing.expectEqual(Type.Map, di1.getType()); 736 | var ai1 = di1.map().?; 737 | try std.testing.expectEqual(ai1.next(), null); 738 | try std.testing.expectEqual(ai1.next(), null); 739 | 740 | const di2 = try DataItem.new("\xa2\x01\x02\x03\x04"); 741 | try std.testing.expectEqual(Type.Map, di2.getType()); 742 | var ai2 = di2.map().?; 743 | const kv1 = ai2.next().?; 744 | try std.testing.expectEqual(kv1.key.int().?, 1); 745 | try std.testing.expectEqual(kv1.value.int().?, 2); 746 | const kv2 = ai2.next().?; 747 | try std.testing.expectEqual(kv2.key.int().?, 3); 748 | try std.testing.expectEqual(kv2.value.int().?, 4); 749 | try std.testing.expectEqual(ai2.next(), null); 750 | 751 | const di3 = try DataItem.new("\xa2\x61\x61\x01\x61\x62\x82\x02\x03"); 752 | try std.testing.expectEqual(Type.Map, di3.getType()); 753 | var ai3 = di3.map().?; 754 | const kv1_2 = ai3.next().?; 755 | try std.testing.expectEqualStrings("a", kv1_2.key.string().?); 756 | try std.testing.expectEqual(kv1_2.value.int().?, 1); 757 | const kv2_2 = ai3.next().?; 758 | try std.testing.expectEqualStrings("b", kv2_2.key.string().?); 759 | var ai3_1 = kv2_2.value.array().?; 760 | try std.testing.expectEqual(ai3_1.next().?.int().?, 2); 761 | try std.testing.expectEqual(ai3_1.next().?.int().?, 3); 762 | try std.testing.expectEqual(ai3_1.next(), null); 763 | try std.testing.expectEqual(ai3.next(), null); 764 | } 765 | 766 | test "deserialize other" { 767 | const di1 = try DataItem.new("\x82\x61\x61\xa1\x61\x62\x61\x63"); 768 | try std.testing.expectEqual(Type.Array, di1.getType()); 769 | var ai1 = di1.array().?; 770 | try std.testing.expectEqualStrings("a", ai1.next().?.string().?); 771 | var m1 = ai1.next().?.map().?; 772 | var kv1 = m1.next().?; 773 | try std.testing.expectEqualStrings("b", kv1.key.string().?); 774 | try std.testing.expectEqualStrings("c", kv1.value.string().?); 775 | } 776 | 777 | test "deserialize simple" { 778 | const di1 = try DataItem.new("\xf4"); 779 | try std.testing.expectEqual(Type.False, di1.getType()); 780 | try std.testing.expectEqual(di1.boolean().?, false); 781 | 782 | const di2 = try DataItem.new("\xf5"); 783 | try std.testing.expectEqual(Type.True, di2.getType()); 784 | try std.testing.expectEqual(di2.boolean().?, true); 785 | 786 | const di3 = try DataItem.new("\xf6"); 787 | try std.testing.expectEqual(Type.Null, di3.getType()); 788 | 789 | const di4 = try DataItem.new("\xf7"); 790 | try std.testing.expectEqual(Type.Undefined, di4.getType()); 791 | 792 | const di5 = try DataItem.new("\xf0"); 793 | try std.testing.expectEqual(Type.Simple, di5.getType()); 794 | try std.testing.expectEqual(di5.simple().?, 16); 795 | 796 | const di6 = try DataItem.new("\xf8\xff"); 797 | try std.testing.expectEqual(Type.Simple, di6.getType()); 798 | try std.testing.expectEqual(di6.simple().?, 255); 799 | } 800 | 801 | test "deserialize float" { 802 | const di1 = try DataItem.new("\xfb\x3f\xf1\x99\x99\x99\x99\x99\x9a"); 803 | try std.testing.expectEqual(Type.Float, di1.getType()); 804 | try std.testing.expectApproxEqAbs(di1.float().?, 1.1, 0.000000001); 805 | 806 | const di2 = try DataItem.new("\xf9\x3e\x00"); 807 | try std.testing.expectEqual(Type.Float, di2.getType()); 808 | try std.testing.expectApproxEqAbs(di2.float().?, 1.5, 0.000000001); 809 | 810 | const di3 = try DataItem.new("\xf9\x80\x00"); 811 | try std.testing.expectEqual(Type.Float, di3.getType()); 812 | try std.testing.expectApproxEqAbs(di3.float().?, -0.0, 0.000000001); 813 | 814 | const di4 = try DataItem.new("\xfb\x7e\x37\xe4\x3c\x88\x00\x75\x9c"); 815 | try std.testing.expectEqual(Type.Float, di4.getType()); 816 | try std.testing.expectApproxEqAbs(di4.float().?, 1.0e+300, 0.000000001); 817 | 818 | try std.testing.expectEqual(@as(f16, 0.0), (try DataItem.new("\xf9\x00\x00")).getFloat16().?); 819 | try std.testing.expectEqual(@as(f16, -0.0), (try DataItem.new("\xf9\x80\x00")).getFloat16().?); 820 | try std.testing.expectEqual(@as(f16, 65504.0), (try DataItem.new("\xf9\x7b\xff")).getFloat16().?); 821 | try std.testing.expectEqual(@as(f32, 3.4028234663852886e+38), (try DataItem.new("\xfa\x7f\x7f\xff\xff")).getFloat32().?); 822 | try std.testing.expectEqual(@as(f64, -4.1), (try DataItem.new("\xfb\xc0\x10\x66\x66\x66\x66\x66\x66")).getFloat64().?); 823 | } 824 | 825 | test "deserialize tagged" { 826 | const di1 = try DataItem.new("\xc0\x74\x32\x30\x31\x33\x2d\x30\x33\x2d\x32\x31\x54\x32\x30\x3a\x30\x34\x3a\x30\x30\x5a"); 827 | try std.testing.expectEqual(Type.Tagged, di1.getType()); 828 | const t1 = di1.tagged().?; 829 | try std.testing.expectEqual(t1.nr, 0); 830 | } 831 | 832 | fn validateTest(data: []const u8, expected: bool) !void { 833 | var i: usize = 0; 834 | try std.testing.expectEqual(expected, validate(data, &i, true)); 835 | } 836 | 837 | test "well formed" { 838 | try validateTest("\x00", true); 839 | try validateTest("\x01", true); 840 | try validateTest("\x0a", true); 841 | try validateTest("\x17", true); 842 | try validateTest("\x18\x18", true); 843 | try validateTest("\x18\x19", true); 844 | try validateTest("\x18\x64", true); 845 | try validateTest("\x19\x03\xe8", true); 846 | 847 | // Indefinite length arrays 848 | try validateTest("\x9f\x01\xff", true); 849 | try validateTest("\x9f\x01\x9f\x02\x9f\x9f\xff\xff\xff\xff", true); 850 | try validateTest("\x9f\x82\x02\x03\x9f\x04\x05\xff\xff", true); 851 | try validateTest("\x9f\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\xff", true); 852 | 853 | // Indefinite length maps 854 | try validateTest("\xbf\x63\x46\x75\x6e\x01\x63\x41\x6d\x74\x21\xff", true); 855 | try validateTest("\xbf\x61\x61\x9f\x03\x04\xff\x61\x62\x9f\x02\x03\xff\xff", true); 856 | try validateTest("\x9f\xbf\x61\x61\x61\x63\xff\xbf\x61\x62\x61\x64\xff\xff", true); 857 | } 858 | 859 | test "malformed" { 860 | // Empty 861 | try validateTest("", false); 862 | 863 | // End of input in a head 864 | try validateTest("\x18", false); 865 | try validateTest("\x19", false); 866 | try validateTest("\x1a", false); 867 | try validateTest("\x1b", false); 868 | try validateTest("\x19\x01", false); 869 | try validateTest("\x1a\x01\x02", false); 870 | try validateTest("\x1b\x01\x02\x03\x04\x05\x06\x07", false); 871 | try validateTest("\x38", false); 872 | try validateTest("\x58", false); 873 | try validateTest("\x78", false); 874 | try validateTest("\x98", false); 875 | try validateTest("\x9a\x01\xff\x00", false); 876 | try validateTest("\xb8", false); 877 | try validateTest("\xd8", false); 878 | try validateTest("\xf8", false); 879 | try validateTest("\xf9\x00", false); 880 | try validateTest("\xfa\x00\x00", false); 881 | try validateTest("\xfb\x00\x00\x00", false); 882 | 883 | // Definite-length strings with short data 884 | try validateTest("\x41", false); 885 | try validateTest("\x61", false); 886 | try validateTest("\x5a\xff\xff\xff\xff\x00", false); 887 | //try validateTest("\x5b\xff\xff\xff\xff\xff\xff\xff\xff\x01\x02\x03", false); TODO: crashes 888 | try validateTest("\x7a\xff\xff\xff\xff\x00", false); 889 | try validateTest("\x7b\x7f\xff\xff\xff\xff\xff\xff\xff\x01\x02\x03", false); 890 | 891 | // Definite-length maps and arrays not closed with enough items 892 | try validateTest("\x81", false); 893 | try validateTest("\x81\x81\x81\x81\x81\x81\x81\x81\x81", false); 894 | try validateTest("\x82\x00", false); 895 | try validateTest("\xa1", false); 896 | try validateTest("\xa2\x01\x02", false); 897 | try validateTest("\xa1\x00", false); 898 | try validateTest("\xa2\x00\x00\x00", false); 899 | 900 | // Tag number not followed by tag content 901 | try validateTest("\xc0", false); 902 | 903 | // Reserved additional information values 904 | try validateTest("\x1c", false); 905 | try validateTest("\x1d", false); 906 | try validateTest("\x1e", false); 907 | try validateTest("\x3c", false); 908 | try validateTest("\x3d", false); 909 | try validateTest("\x3e", false); 910 | try validateTest("\x5c", false); 911 | try validateTest("\x5d", false); 912 | try validateTest("\x5e", false); 913 | try validateTest("\x7c", false); 914 | try validateTest("\x7d", false); 915 | try validateTest("\x7e", false); 916 | try validateTest("\x9c", false); 917 | try validateTest("\x9d", false); 918 | try validateTest("\x9e", false); 919 | try validateTest("\xbc", false); 920 | try validateTest("\xbd", false); 921 | try validateTest("\xbe", false); 922 | try validateTest("\xdc", false); 923 | try validateTest("\xdd", false); 924 | try validateTest("\xde", false); 925 | try validateTest("\xfc", false); 926 | try validateTest("\xfd", false); 927 | try validateTest("\xfe", false); 928 | 929 | // Reserved two-byte encodings of simple values 930 | try validateTest("\xf8\x00", false); 931 | try validateTest("\xf8\x01", false); 932 | try validateTest("\xf8\x18", false); 933 | try validateTest("\xf8\x1f", false); 934 | 935 | // Break occuring on its own outside of an indifinite-length item 936 | try validateTest("\xff", false); 937 | 938 | // Break occuring in a definite-length array or map or a tag 939 | try validateTest("\x81\xff", false); 940 | try validateTest("\x82\x00\xff", false); 941 | try validateTest("\xa1\xff", false); 942 | try validateTest("\xa1\xff\x00", false); 943 | try validateTest("\xa1\x00\xff", false); 944 | try validateTest("\xa2\x00\x00\xff", false); 945 | 946 | // Break missing in a indefinate-length array or map 947 | try validateTest("\x9f\x82\x02\x03\x9f\x04\x05\xff", false); 948 | try validateTest("\x9f\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", false); 949 | try validateTest("\xbf\x63\x46\x75\x6e\x01\x63\x41\x6d\x74\x21", false); 950 | try validateTest("\xbf\x61\x61\x9f\x03\x04\xff\x61\x62\x9f\x02\x03\xff", false); 951 | try validateTest("\xbf\x61\x61\x9f\x03\x04\x61\x62\x9f\x02\x03\xff\xff", false); 952 | try validateTest("\x9f\xbf\x61\x61\x61\x63\xbf\x61\x62\x61\x64\xff\xff", false); 953 | try validateTest("\x9f\xbf\x61\x61\x61\x63\xff\xbf\x61\x62\x61\x64\xff", false); 954 | try validateTest("\x9f\xbf\x61\x61\x61\x63\xff\xbf\x61\x62\x61\x64", false); 955 | } 956 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const testing = std.testing; 4 | 5 | const core = @import("core.zig"); 6 | const CborError = core.CborError; 7 | const Pair = core.Pair; 8 | const Tag = core.Tag; 9 | const FloatTag = core.FloatTag; 10 | const Float = core.Float; 11 | const SimpleValue = core.SimpleValue; 12 | const DataItemTag = core.DataItemTag; 13 | const DataItem = core.DataItem; 14 | 15 | const encode = @import("encoder.zig").encode; 16 | const encodeAlloc = @import("encoder.zig").encodeAlloc; 17 | const decode = @import("decoder.zig").decode; 18 | 19 | const TestError = CborError || error{ TestExpectedEqual, TestUnexpectedResult }; 20 | 21 | fn test_data_item(data: []const u8, expected: DataItem) TestError!void { 22 | const allocator = std.testing.allocator; 23 | const dip = try decode(allocator, data); 24 | defer dip.deinit(allocator); 25 | try std.testing.expectEqual(expected, dip); 26 | } 27 | 28 | fn test_data_item_eql(data: []const u8, expected: *const DataItem) TestError!void { 29 | const allocator = std.testing.allocator; 30 | const dip = try decode(allocator, data); 31 | defer dip.deinit(allocator); 32 | try std.testing.expect(expected.*.equal(&dip)); 33 | } 34 | 35 | test "DataItem.equal test" { 36 | const di1 = DataItem{ .int = 10 }; 37 | const di2 = DataItem{ .int = 23 }; 38 | const di3 = DataItem{ .int = 23 }; 39 | const di4 = DataItem{ .int = -9988776655 }; 40 | 41 | try std.testing.expect(!di1.equal(&di2)); 42 | try std.testing.expect(di2.equal(&di3)); 43 | try std.testing.expect(!di1.equal(&di4)); 44 | try std.testing.expect(!di2.equal(&di4)); 45 | try std.testing.expect(!di3.equal(&di4)); 46 | 47 | var allocator = std.testing.allocator; 48 | 49 | var di5 = try DataItem.bytes(&.{10}, .{ .allocator = allocator }); 50 | defer di5.deinit(allocator); 51 | 52 | try std.testing.expect(!di5.equal(&di1)); 53 | try std.testing.expect(!di1.equal(&di5)); 54 | try std.testing.expect(di5.equal(&di5)); 55 | 56 | var di6 = try DataItem.bytes(&.{10}, .{ .allocator = allocator }); 57 | defer di6.deinit(allocator); 58 | 59 | try std.testing.expect(di5.equal(&di6)); 60 | } 61 | 62 | test "MT0: decode cbor unsigned integer value" { 63 | try test_data_item(&.{0x00}, DataItem{ .int = 0 }); 64 | try test_data_item(&.{0x01}, DataItem{ .int = 1 }); 65 | try test_data_item(&.{0x0a}, DataItem{ .int = 10 }); 66 | try test_data_item(&.{0x17}, DataItem{ .int = 23 }); 67 | try test_data_item(&.{ 0x18, 0x18 }, DataItem{ .int = 24 }); 68 | try test_data_item(&.{ 0x18, 0x19 }, DataItem{ .int = 25 }); 69 | try test_data_item(&.{ 0x18, 0x64 }, DataItem{ .int = 100 }); 70 | try test_data_item(&.{ 0x18, 0x7b }, DataItem{ .int = 123 }); 71 | try test_data_item(&.{ 0x19, 0x03, 0xe8 }, DataItem{ .int = 1000 }); 72 | try test_data_item(&.{ 0x19, 0x04, 0xd2 }, DataItem{ .int = 1234 }); 73 | try test_data_item(&.{ 0x1a, 0x00, 0x01, 0xe2, 0x40 }, DataItem{ .int = 123456 }); 74 | try test_data_item(&.{ 0x1a, 0x00, 0x0f, 0x42, 0x40 }, DataItem{ .int = 1000000 }); 75 | try test_data_item(&.{ 0x1b, 0x00, 0x00, 0x00, 0x02, 0xdf, 0xdc, 0x1c, 0x34 }, DataItem{ .int = 12345678900 }); 76 | try test_data_item(&.{ 0x1b, 0x00, 0x00, 0x00, 0xe8, 0xd4, 0xa5, 0x10, 0x00 }, DataItem{ .int = 1000000000000 }); 77 | try test_data_item(&.{ 0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, DataItem{ .int = 18446744073709551615 }); 78 | } 79 | 80 | test "MT1: decode cbor signed integer value" { 81 | try test_data_item(&.{0x20}, DataItem{ .int = -1 }); 82 | try test_data_item(&.{0x22}, DataItem{ .int = -3 }); 83 | try test_data_item(&.{ 0x38, 0x63 }, DataItem{ .int = -100 }); 84 | try test_data_item(&.{ 0x39, 0x01, 0xf3 }, DataItem{ .int = -500 }); 85 | try test_data_item(&.{ 0x39, 0x03, 0xe7 }, DataItem{ .int = -1000 }); 86 | try test_data_item(&.{ 0x3a, 0x00, 0x0f, 0x3d, 0xdc }, DataItem{ .int = -998877 }); 87 | try test_data_item(&.{ 0x3b, 0x00, 0x00, 0x00, 0x02, 0x53, 0x60, 0xa2, 0xce }, DataItem{ .int = -9988776655 }); 88 | try test_data_item(&.{ 0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, DataItem{ .int = -18446744073709551616 }); 89 | } 90 | 91 | test "MT2: decode cbor byte string" { 92 | const allocator = std.testing.allocator; 93 | 94 | try test_data_item_eql(&.{0b01000000}, &DataItem{ .bytes = &.{} }); 95 | 96 | var di1 = try DataItem.bytes(&.{10}, .{ .allocator = allocator }); 97 | defer di1.deinit(allocator); 98 | try test_data_item_eql(&.{ 0b01000001, 0x0a }, &di1); 99 | 100 | var di2 = try DataItem.bytes(&.{ 10, 11, 12, 13, 14 }, .{ .allocator = allocator }); 101 | defer di2.deinit(allocator); 102 | try test_data_item_eql(&.{ 0b01000101, 0x0a, 0xb, 0xc, 0xd, 0xe }, &di2); 103 | 104 | try std.testing.expectError(CborError.Malformed, decode(allocator, &.{ 0b01000011, 0x0a })); 105 | try std.testing.expectError(CborError.Malformed, decode(allocator, &.{ 0b01000101, 0x0a, 0xb, 0xc })); 106 | } 107 | 108 | test "MT3: decode cbor text string" { 109 | const allocator = std.testing.allocator; 110 | 111 | try test_data_item(&.{0x60}, try DataItem.text(&.{}, .{ .allocator = allocator })); 112 | 113 | const exp1 = try DataItem.text("a", .{ .allocator = allocator }); 114 | defer exp1.deinit(allocator); 115 | const di1 = try decode(allocator, &.{ 0x61, 0x61 }); 116 | defer di1.deinit(allocator); 117 | try std.testing.expectEqualSlices(u8, exp1.text, di1.text); 118 | try std.testing.expect(exp1.equal(&di1)); 119 | 120 | const exp2 = try DataItem.text("IETF", .{ .allocator = allocator }); 121 | defer exp2.deinit(allocator); 122 | const di2 = try decode(allocator, &.{ 0x64, 0x49, 0x45, 0x54, 0x46 }); 123 | defer di2.deinit(allocator); 124 | try std.testing.expectEqualSlices(u8, exp2.text, di2.text); 125 | try std.testing.expect(exp2.equal(&di2)); 126 | 127 | const exp3 = try DataItem.text("\"\\", .{ .allocator = allocator }); 128 | defer exp3.deinit(allocator); 129 | const di3 = try decode(allocator, &.{ 0x62, 0x22, 0x5c }); 130 | defer di3.deinit(allocator); 131 | try std.testing.expectEqualSlices(u8, exp3.text, di3.text); 132 | try std.testing.expect(exp3.equal(&di3)); 133 | 134 | try std.testing.expect(!exp1.equal(&di2)); 135 | try std.testing.expect(!exp1.equal(&di3)); 136 | try std.testing.expect(!exp2.equal(&di3)); 137 | 138 | // TODO: test unicode https://www.rfc-editor.org/rfc/rfc8949.html#name-examples-of-encoded-cbor-da 139 | } 140 | 141 | test "MT4: decode cbor array" { 142 | const allocator = std.testing.allocator; 143 | 144 | const exp1 = try DataItem.array(&.{}, .{ .allocator = allocator }); 145 | defer exp1.deinit(allocator); 146 | const di1 = try decode(allocator, &.{0x80}); 147 | defer di1.deinit(allocator); 148 | try std.testing.expect(exp1.equal(&di1)); 149 | 150 | const exp2 = try DataItem.array(&.{ DataItem.int(1), DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator }); 151 | defer exp2.deinit(allocator); 152 | const di2 = try decode(allocator, &.{ 0x83, 0x01, 0x02, 0x03 }); 153 | defer di2.deinit(allocator); 154 | try std.testing.expect(exp2.equal(&di2)); 155 | 156 | const exp3 = try DataItem.array(&.{ DataItem.int(1), try DataItem.array(&.{ DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator }), try DataItem.array(&.{ DataItem.int(4), DataItem.int(5) }, .{ .allocator = allocator }) }, .{ .allocator = allocator }); 157 | defer exp3.deinit(allocator); 158 | const di3 = try decode(allocator, &.{ 0x83, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05 }); 159 | defer di3.deinit(allocator); 160 | try std.testing.expect(exp3.equal(&di3)); 161 | 162 | const exp4 = try DataItem.array(&.{ DataItem.int(1), DataItem.int(2), DataItem.int(3), DataItem.int(4), DataItem.int(5), DataItem.int(6), DataItem.int(7), DataItem.int(8), DataItem.int(9), DataItem.int(10), DataItem.int(11), DataItem.int(12), DataItem.int(13), DataItem.int(14), DataItem.int(15), DataItem.int(16), DataItem.int(17), DataItem.int(18), DataItem.int(19), DataItem.int(20), DataItem.int(21), DataItem.int(22), DataItem.int(23), DataItem.int(24), DataItem.int(25) }, .{ .allocator = allocator }); 163 | defer exp4.deinit(allocator); 164 | const di4 = try decode(allocator, &.{ 0x98, 0x19, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x18, 0x18, 0x19 }); 165 | defer di4.deinit(allocator); 166 | try std.testing.expect(exp4.equal(&di4)); 167 | 168 | try std.testing.expect(!exp1.equal(&di2)); 169 | try std.testing.expect(!exp1.equal(&di3)); 170 | try std.testing.expect(!exp1.equal(&di4)); 171 | try std.testing.expect(!exp2.equal(&di3)); 172 | try std.testing.expect(!exp2.equal(&di4)); 173 | try std.testing.expect(!exp3.equal(&di4)); 174 | } 175 | 176 | test "MT5: decode empty cbor map" { 177 | const allocator = std.testing.allocator; 178 | 179 | const exp1 = try DataItem.map(&.{}, .{ .allocator = allocator }); 180 | defer exp1.deinit(allocator); 181 | const di1 = try decode(allocator, &.{0xa0}); 182 | defer di1.deinit(allocator); 183 | try std.testing.expect(exp1.equal(&di1)); 184 | } 185 | 186 | test "MT5: decode cbor map {1:2,3:4}" { 187 | const allocator = std.testing.allocator; 188 | 189 | const exp1 = try DataItem.map(&.{ Pair.new(DataItem.int(1), DataItem.int(2)), Pair.new(DataItem.int(3), DataItem.int(4)) }, .{ .allocator = allocator }); 190 | defer exp1.deinit(allocator); 191 | const di1 = try decode(allocator, &.{ 0xa2, 0x01, 0x02, 0x03, 0x04 }); 192 | defer di1.deinit(allocator); 193 | try std.testing.expect(exp1.equal(&di1)); 194 | } 195 | 196 | test "MT5: decode cbor map {\"a\":1,\"b\":[2,3]}" { 197 | const allocator = std.testing.allocator; 198 | 199 | const exp1 = try DataItem.map(&.{ Pair.new(try DataItem.text("a", .{ .allocator = allocator }), DataItem.int(1)), Pair.new(try DataItem.text("b", .{ .allocator = allocator }), try DataItem.array(&.{ DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator })) }, .{ .allocator = allocator }); 200 | defer exp1.deinit(allocator); 201 | const di1 = try decode(allocator, &.{ 0xa2, 0x61, 0x61, 0x01, 0x61, 0x62, 0x82, 0x02, 0x03 }); 202 | defer di1.deinit(allocator); 203 | try std.testing.expect(exp1.equal(&di1)); 204 | } 205 | 206 | test "MT5: decode cbor map within array [\"a\",{\"b\":\"c\"}]" { 207 | const allocator = std.testing.allocator; 208 | 209 | const exp1 = try DataItem.array(&.{ try DataItem.text("a", .{ .allocator = allocator }), try DataItem.map(&.{Pair.new(try DataItem.text("b", .{ .allocator = allocator }), try DataItem.text("c", .{ .allocator = allocator }))}, .{ .allocator = allocator }) }, .{ .allocator = allocator }); 210 | defer exp1.deinit(allocator); 211 | const di1 = try decode(allocator, &.{ 0x82, 0x61, 0x61, 0xa1, 0x61, 0x62, 0x61, 0x63 }); 212 | defer di1.deinit(allocator); 213 | try std.testing.expect(exp1.equal(&di1)); 214 | } 215 | 216 | test "MT5: decode cbor map of text pairs" { 217 | const allocator = std.testing.allocator; 218 | 219 | const exp1 = try DataItem.map(&.{ Pair.new(try DataItem.text("a", .{ .allocator = allocator }), try DataItem.text("A", .{ .allocator = allocator })), Pair.new(try DataItem.text("b", .{ .allocator = allocator }), try DataItem.text("B", .{ .allocator = allocator })), Pair.new(try DataItem.text("c", .{ .allocator = allocator }), try DataItem.text("C", .{ .allocator = allocator })), Pair.new(try DataItem.text("d", .{ .allocator = allocator }), try DataItem.text("D", .{ .allocator = allocator })), Pair.new(try DataItem.text("e", .{ .allocator = allocator }), try DataItem.text("E", .{ .allocator = allocator })) }, .{ .allocator = allocator }); 220 | defer exp1.deinit(allocator); 221 | const di1 = try decode(allocator, &.{ 0xa5, 0x61, 0x61, 0x61, 0x41, 0x61, 0x62, 0x61, 0x42, 0x61, 0x63, 0x61, 0x43, 0x61, 0x64, 0x61, 0x44, 0x61, 0x65, 0x61, 0x45 }); 222 | defer di1.deinit(allocator); 223 | try std.testing.expect(exp1.equal(&di1)); 224 | } 225 | 226 | test "MT6: decode cbor tagged data item 1(1363896240)" { 227 | const allocator = std.testing.allocator; 228 | 229 | const exp1 = try DataItem.tagged(allocator, 1, DataItem.int(1363896240)); 230 | defer exp1.deinit(allocator); 231 | const di1 = try decode(allocator, &.{ 0xc1, 0x1a, 0x51, 0x4b, 0x67, 0xb0 }); 232 | defer di1.deinit(allocator); 233 | try std.testing.expect(exp1.equal(&di1)); 234 | } 235 | 236 | test "MT6: decode cbor tagged data item 32(\"http://www.example.com\")" { 237 | const allocator = std.testing.allocator; 238 | 239 | const exp1 = try DataItem.tagged(allocator, 32, try DataItem.text("http://www.example.com", .{ .allocator = allocator })); 240 | defer exp1.deinit(allocator); 241 | const di1 = try decode(allocator, &.{ 0xd8, 0x20, 0x76, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d }); 242 | defer di1.deinit(allocator); 243 | try std.testing.expect(exp1.equal(&di1)); 244 | try std.testing.expectEqualStrings(exp1.tag.content.text, di1.tag.content.text); 245 | } 246 | 247 | test "MT7: decode f16 0.0" { 248 | const allocator = std.testing.allocator; 249 | 250 | var expected = DataItem.float16(0.0); 251 | var ne1 = DataItem.float16(0.1); 252 | var ne2 = DataItem.float16(-0.1); 253 | var ne3 = DataItem.float32(0.0); 254 | var ne4 = DataItem.float64(0.0); 255 | var di = try decode(allocator, &.{ 0xf9, 0x00, 0x00 }); 256 | 257 | try std.testing.expect(di.equal(&expected)); 258 | try std.testing.expect(!di.equal(&ne1)); 259 | try std.testing.expect(!di.equal(&ne2)); 260 | try std.testing.expect(!di.equal(&ne3)); 261 | try std.testing.expect(!di.equal(&ne4)); 262 | } 263 | 264 | test "MT7: decode f16 -0.0" { 265 | const allocator = std.testing.allocator; 266 | 267 | var expected = DataItem{ .float = Float{ .float16 = -0.0 } }; 268 | var di = try decode(allocator, &.{ 0xf9, 0x80, 0x00 }); 269 | 270 | try std.testing.expectEqual(expected.float.float16, di.float.float16); 271 | //try std.testing.expect(di.equal(&expected)); 272 | } 273 | 274 | test "MT7: decode f16 1.0" { 275 | const allocator = std.testing.allocator; 276 | 277 | var expected = DataItem{ .float = Float{ .float16 = 1.0 } }; 278 | var di = try decode(allocator, &.{ 0xf9, 0x3c, 0x00 }); 279 | 280 | try std.testing.expect(di.equal(&expected)); 281 | } 282 | 283 | test "MT7: decode f16 1.5" { 284 | const allocator = std.testing.allocator; 285 | 286 | var expected = DataItem{ .float = Float{ .float16 = 1.5 } }; 287 | var di = try decode(allocator, &.{ 0xf9, 0x3e, 0x00 }); 288 | 289 | try std.testing.expect(di.equal(&expected)); 290 | } 291 | 292 | test "MT7: decode f16 5.960464477539063e-8" { 293 | const allocator = std.testing.allocator; 294 | 295 | var expected = DataItem{ .float = Float{ .float16 = 5.960464477539063e-8 } }; 296 | var di = try decode(allocator, &.{ 0xf9, 0x00, 0x01 }); 297 | 298 | try std.testing.expect(di.equal(&expected)); 299 | } 300 | 301 | test "MT7: decode f16 0.00006103515625" { 302 | const allocator = std.testing.allocator; 303 | 304 | var expected = DataItem{ .float = Float{ .float16 = 0.00006103515625 } }; 305 | var di = try decode(allocator, &.{ 0xf9, 0x04, 0x00 }); 306 | 307 | try std.testing.expect(di.equal(&expected)); 308 | } 309 | 310 | test "MT7: decode f16 -4.0" { 311 | const allocator = std.testing.allocator; 312 | 313 | var expected = DataItem{ .float = Float{ .float16 = -4.0 } }; 314 | var di = try decode(allocator, &.{ 0xf9, 0xc4, 0x00 }); 315 | 316 | try std.testing.expect(di.equal(&expected)); 317 | } 318 | 319 | test "MT7: decode f32 100000.0" { 320 | const allocator = std.testing.allocator; 321 | 322 | var expected = DataItem{ .float = Float{ .float32 = 100000.0 } }; 323 | var di = try decode(allocator, &.{ 0xfa, 0x47, 0xc3, 0x50, 0x00 }); 324 | 325 | try std.testing.expect(di.equal(&expected)); 326 | } 327 | 328 | test "MT7: decode f32 3.4028234663852886e+38" { 329 | const allocator = std.testing.allocator; 330 | 331 | var expected = DataItem{ .float = Float{ .float32 = 3.4028234663852886e+38 } }; 332 | var di = try decode(allocator, &.{ 0xfa, 0x7f, 0x7f, 0xff, 0xff }); 333 | 334 | try std.testing.expect(di.equal(&expected)); 335 | } 336 | 337 | test "MT7: decode f64 1.1" { 338 | const allocator = std.testing.allocator; 339 | 340 | var expected = DataItem{ .float = Float{ .float64 = 1.1 } }; 341 | var di = try decode(allocator, &.{ 0xfb, 0x3f, 0xf1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a }); 342 | 343 | try std.testing.expect(di.equal(&expected)); 344 | } 345 | 346 | test "MT7: decode f64 1.0e+300" { 347 | const allocator = std.testing.allocator; 348 | 349 | var expected = DataItem{ .float = Float{ .float64 = 1.0e+300 } }; 350 | var di = try decode(allocator, &.{ 0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c }); 351 | 352 | try std.testing.expect(di.equal(&expected)); 353 | } 354 | 355 | test "MT7: decode f64 -4.1" { 356 | const allocator = std.testing.allocator; 357 | 358 | var expected = DataItem{ .float = Float{ .float64 = -4.1 } }; 359 | var di = try decode(allocator, &.{ 0xfb, 0xc0, 0x10, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66 }); 360 | 361 | try std.testing.expect(di.equal(&expected)); 362 | } 363 | 364 | test "MT7: simple value" { 365 | const allocator = std.testing.allocator; 366 | 367 | var expected1 = DataItem.False(); 368 | var di1 = try decode(allocator, &.{0xf4}); 369 | try std.testing.expect(di1.equal(&expected1)); 370 | 371 | var expected2 = DataItem.True(); 372 | var di2 = try decode(allocator, &.{0xf5}); 373 | try std.testing.expect(di2.equal(&expected2)); 374 | 375 | var expected3 = DataItem.Null(); 376 | var di3 = try decode(allocator, &.{0xf6}); 377 | try std.testing.expect(di3.equal(&expected3)); 378 | 379 | var expected4 = DataItem.Undefined(); 380 | var di4 = try decode(allocator, &.{0xf7}); 381 | try std.testing.expect(di4.equal(&expected4)); 382 | } 383 | 384 | test "decode WebAuthn attestationObject" { 385 | const allocator = std.testing.allocator; 386 | const attestationObject = try std.fs.cwd().openFile("data/WebAuthnCreate.dat", .{ .mode = .read_only }); 387 | defer attestationObject.close(); 388 | const bytes = try attestationObject.readToEndAlloc(allocator, 4096); 389 | defer allocator.free(bytes); 390 | 391 | var di = try decode(allocator, bytes); 392 | defer di.deinit(allocator); 393 | 394 | try std.testing.expect(di.isMap()); 395 | 396 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 397 | const fmt = di.getValueByString("fmt"); 398 | try std.testing.expect(fmt.?.isText()); 399 | try std.testing.expectEqualStrings("fido-u2f", fmt.?.text); 400 | 401 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 402 | const attStmt = di.getValueByString("attStmt"); 403 | try std.testing.expect(attStmt.?.isMap()); 404 | const authData = di.getValueByString("authData"); 405 | try std.testing.expect(authData.?.isBytes()); 406 | 407 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 408 | try std.testing.expectEqual(@as(usize, 196), authData.?.bytes.len); 409 | 410 | // ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 411 | const sig = attStmt.?.getValueByString("sig"); 412 | try std.testing.expect(sig.?.isBytes()); 413 | try std.testing.expectEqual(@as(usize, 71), sig.?.bytes.len); 414 | 415 | const x5c = attStmt.?.getValueByString("x5c"); 416 | try std.testing.expect(x5c.?.isArray()); 417 | try std.testing.expectEqual(@as(usize, 1), x5c.?.array.len); 418 | 419 | const x5c_stmt = x5c.?.get(0); 420 | try std.testing.expect(x5c_stmt.?.isBytes()); 421 | try std.testing.expectEqual(@as(usize, 704), x5c_stmt.?.bytes.len); 422 | } 423 | 424 | test "MT0: encode cbor unsigned integer value" { 425 | const allocator = std.testing.allocator; 426 | 427 | const di1 = DataItem{ .int = 0 }; 428 | const cbor1 = try encodeAlloc(allocator, &di1); 429 | defer allocator.free(cbor1); 430 | try std.testing.expectEqualSlices(u8, &.{0x00}, cbor1); 431 | 432 | const di2 = DataItem{ .int = 23 }; 433 | const cbor2 = try encodeAlloc(allocator, &di2); 434 | defer allocator.free(cbor2); 435 | try std.testing.expectEqualSlices(u8, &.{0x17}, cbor2); 436 | 437 | const di3 = DataItem{ .int = 24 }; 438 | const cbor3 = try encodeAlloc(allocator, &di3); 439 | defer allocator.free(cbor3); 440 | try std.testing.expectEqualSlices(u8, &.{ 0x18, 0x18 }, cbor3); 441 | 442 | const di4 = DataItem{ .int = 255 }; 443 | const cbor4 = try encodeAlloc(allocator, &di4); 444 | defer allocator.free(cbor4); 445 | try std.testing.expectEqualSlices(u8, &.{ 0x18, 0xff }, cbor4); 446 | 447 | const di5 = DataItem{ .int = 256 }; 448 | const cbor5 = try encodeAlloc(allocator, &di5); 449 | defer allocator.free(cbor5); 450 | try std.testing.expectEqualSlices(u8, &.{ 0x19, 0x01, 0x00 }, cbor5); 451 | 452 | const di6 = DataItem{ .int = 1000 }; 453 | const cbor6 = try encodeAlloc(allocator, &di6); 454 | defer allocator.free(cbor6); 455 | try std.testing.expectEqualSlices(u8, &.{ 0x19, 0x03, 0xe8 }, cbor6); 456 | 457 | const di7 = DataItem{ .int = 65535 }; 458 | const cbor7 = try encodeAlloc(allocator, &di7); 459 | defer allocator.free(cbor7); 460 | try std.testing.expectEqualSlices(u8, &.{ 0x19, 0xff, 0xff }, cbor7); 461 | 462 | const di8 = DataItem{ .int = 65536 }; 463 | const cbor8 = try encodeAlloc(allocator, &di8); 464 | defer allocator.free(cbor8); 465 | try std.testing.expectEqualSlices(u8, &.{ 0x1a, 0x00, 0x01, 0x00, 0x00 }, cbor8); 466 | 467 | const di9 = DataItem{ .int = 4294967295 }; 468 | const cbor9 = try encodeAlloc(allocator, &di9); 469 | defer allocator.free(cbor9); 470 | try std.testing.expectEqualSlices(u8, &.{ 0x1a, 0xff, 0xff, 0xff, 0xff }, cbor9); 471 | 472 | const di10 = DataItem{ .int = 12345678900 }; 473 | const cbor10 = try encodeAlloc(allocator, &di10); 474 | defer allocator.free(cbor10); 475 | try std.testing.expectEqualSlices(u8, &.{ 0x1b, 0x00, 0x00, 0x00, 0x02, 0xdf, 0xdc, 0x1c, 0x34 }, cbor10); 476 | 477 | const di11 = DataItem{ .int = 18446744073709551615 }; 478 | const cbor11 = try encodeAlloc(allocator, &di11); 479 | defer allocator.free(cbor11); 480 | try std.testing.expectEqualSlices(u8, &.{ 0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, cbor11); 481 | } 482 | 483 | test "MT1: encode cbor signed integer value" { 484 | const allocator = std.testing.allocator; 485 | 486 | const di1 = DataItem{ .int = -1 }; 487 | const cbor1 = try encodeAlloc(allocator, &di1); 488 | defer allocator.free(cbor1); 489 | try std.testing.expectEqualSlices(u8, &.{0x20}, cbor1); 490 | 491 | const di2 = DataItem{ .int = -3 }; 492 | const cbor2 = try encodeAlloc(allocator, &di2); 493 | defer allocator.free(cbor2); 494 | try std.testing.expectEqualSlices(u8, &.{0x22}, cbor2); 495 | 496 | const di3 = DataItem{ .int = -100 }; 497 | const cbor3 = try encodeAlloc(allocator, &di3); 498 | defer allocator.free(cbor3); 499 | try std.testing.expectEqualSlices(u8, &.{ 0x38, 0x63 }, cbor3); 500 | 501 | const di4 = DataItem{ .int = -1000 }; 502 | const cbor4 = try encodeAlloc(allocator, &di4); 503 | defer allocator.free(cbor4); 504 | try std.testing.expectEqualSlices(u8, &.{ 0x39, 0x03, 0xe7 }, cbor4); 505 | 506 | const di5 = DataItem{ .int = -998877 }; 507 | const cbor5 = try encodeAlloc(allocator, &di5); 508 | defer allocator.free(cbor5); 509 | try std.testing.expectEqualSlices(u8, &.{ 0x3a, 0x00, 0x0f, 0x3d, 0xdc }, cbor5); 510 | 511 | const di6 = DataItem{ .int = -18446744073709551616 }; 512 | const cbor6 = try encodeAlloc(allocator, &di6); 513 | defer allocator.free(cbor6); 514 | try std.testing.expectEqualSlices(u8, &.{ 0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, cbor6); 515 | } 516 | 517 | test "MT2: encode cbor byte string" { 518 | const allocator = std.testing.allocator; 519 | 520 | var di1 = try DataItem.bytes(&.{}, .{ .allocator = allocator }); 521 | defer di1.deinit(allocator); 522 | const cbor1 = try encodeAlloc(allocator, &di1); 523 | defer allocator.free(cbor1); 524 | try std.testing.expectEqualSlices(u8, &.{0b01000000}, cbor1); 525 | 526 | var di2 = try DataItem.bytes(&.{10}, .{ .allocator = allocator }); 527 | defer di2.deinit(allocator); 528 | const cbor2 = try encodeAlloc(allocator, &di2); 529 | defer allocator.free(cbor2); 530 | try std.testing.expectEqualSlices(u8, &.{ 0x41, 0x0a }, cbor2); 531 | 532 | var di3 = try DataItem.bytes(&.{ 10, 11, 12, 13, 14 }, .{ .allocator = allocator }); 533 | defer di3.deinit(allocator); 534 | const cbor3 = try encodeAlloc(allocator, &di3); 535 | defer allocator.free(cbor3); 536 | try std.testing.expectEqualSlices(u8, &.{ 0x45, 0x0a, 0xb, 0xc, 0xd, 0xe }, cbor3); 537 | 538 | var di4 = try DataItem.bytes(&.{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 }, .{ .allocator = allocator }); 539 | defer di4.deinit(allocator); 540 | const cbor4 = try encodeAlloc(allocator, &di4); 541 | defer allocator.free(cbor4); 542 | try std.testing.expectEqualSlices(u8, &.{ 0x58, 0x19, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 }, cbor4); 543 | } 544 | 545 | test "MT3: encode cbor text string" { 546 | const allocator = std.testing.allocator; 547 | 548 | var di1 = try DataItem.text(&.{}, .{ .allocator = allocator }); 549 | defer di1.deinit(allocator); 550 | const cbor1 = try encodeAlloc(allocator, &di1); 551 | defer allocator.free(cbor1); 552 | try std.testing.expectEqualSlices(u8, &.{0x60}, cbor1); 553 | 554 | var di2 = try DataItem.text("a", .{ .allocator = allocator }); 555 | defer di2.deinit(allocator); 556 | const cbor2 = try encodeAlloc(allocator, &di2); 557 | defer allocator.free(cbor2); 558 | try std.testing.expectEqualSlices(u8, &.{ 0x61, 0x61 }, cbor2); 559 | 560 | var di3 = try DataItem.text("IETF", .{ .allocator = allocator }); 561 | defer di3.deinit(allocator); 562 | const cbor3 = try encodeAlloc(allocator, &di3); 563 | defer allocator.free(cbor3); 564 | try std.testing.expectEqualSlices(u8, &.{ 0x64, 0x49, 0x45, 0x54, 0x46 }, cbor3); 565 | 566 | var di4 = try DataItem.text("\"\\", .{ .allocator = allocator }); 567 | defer di4.deinit(allocator); 568 | const cbor4 = try encodeAlloc(allocator, &di4); 569 | defer allocator.free(cbor4); 570 | try std.testing.expectEqualSlices(u8, &.{ 0x62, 0x22, 0x5c }, cbor4); 571 | 572 | // TODO: test unicode https://www.rfc-editor.org/rfc/rfc8949.html#name-examples-of-encoded-cbor-da 573 | } 574 | 575 | test "MT4: encode cbor array" { 576 | const allocator = std.testing.allocator; 577 | 578 | var di1 = try DataItem.array(&.{}, .{ .allocator = allocator }); 579 | defer di1.deinit(allocator); 580 | const cbor1 = try encodeAlloc(allocator, &di1); 581 | defer allocator.free(cbor1); 582 | try std.testing.expectEqualSlices(u8, &.{0x80}, cbor1); 583 | 584 | var di2 = try DataItem.array(&.{ DataItem.int(1), DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator }); 585 | defer di2.deinit(allocator); 586 | const cbor2 = try encodeAlloc(allocator, &di2); 587 | defer allocator.free(cbor2); 588 | try std.testing.expectEqualSlices(u8, &.{ 0x83, 0x01, 0x02, 0x03 }, cbor2); 589 | 590 | const di3 = try DataItem.array(&.{ DataItem.int(1), try DataItem.array(&.{ DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator }), try DataItem.array(&.{ DataItem.int(4), DataItem.int(5) }, .{ .allocator = allocator }) }, .{ .allocator = allocator }); 591 | defer di3.deinit(allocator); 592 | const cbor3 = try encodeAlloc(allocator, &di3); 593 | defer allocator.free(cbor3); 594 | try std.testing.expectEqualSlices(u8, &.{ 0x83, 0x01, 0x82, 0x02, 0x03, 0x82, 0x04, 0x05 }, cbor3); 595 | 596 | const di4 = try DataItem.array(&.{ DataItem.int(1), DataItem.int(2), DataItem.int(3), DataItem.int(4), DataItem.int(5), DataItem.int(6), DataItem.int(7), DataItem.int(8), DataItem.int(9), DataItem.int(10), DataItem.int(11), DataItem.int(12), DataItem.int(13), DataItem.int(14), DataItem.int(15), DataItem.int(16), DataItem.int(17), DataItem.int(18), DataItem.int(19), DataItem.int(20), DataItem.int(21), DataItem.int(22), DataItem.int(23), DataItem.int(24), DataItem.int(25) }, .{ .allocator = allocator }); 597 | defer di4.deinit(allocator); 598 | const cbor4 = try encodeAlloc(allocator, &di4); 599 | defer allocator.free(cbor4); 600 | try std.testing.expectEqualSlices(u8, &.{ 0x98, 0x19, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x18, 0x18, 0x19 }, cbor4); 601 | } 602 | 603 | test "MT5: encode empty cbor map" { 604 | const allocator = std.testing.allocator; 605 | 606 | var di = try DataItem.map(&.{}, .{ .allocator = allocator }); 607 | defer di.deinit(allocator); 608 | const cbor = try encodeAlloc(allocator, &di); 609 | defer allocator.free(cbor); 610 | try std.testing.expectEqualSlices(u8, &.{0xa0}, cbor); 611 | } 612 | 613 | test "MT5: encode cbor map {1:2,3:4}" { 614 | const allocator = std.testing.allocator; 615 | 616 | var di = try DataItem.map(&.{ Pair.new(DataItem.int(1), DataItem.int(2)), Pair.new(DataItem.int(3), DataItem.int(4)) }, .{ .allocator = allocator }); 617 | defer di.deinit(allocator); 618 | const cbor = try encodeAlloc(allocator, &di); 619 | defer allocator.free(cbor); 620 | try std.testing.expectEqualSlices(u8, &.{ 0xa2, 0x01, 0x02, 0x03, 0x04 }, cbor); 621 | 622 | // Keys should be sorted in asc order. 623 | // TODO: sorting currently disabled (see issues) 624 | // var di2 = try DataItem.map(allocator, &.{ Pair.new(DataItem.int(3), DataItem.int(4)), Pair.new(DataItem.int(1), DataItem.int(2)) }); 625 | // defer di2.deinit(allocator); 626 | // const cbor2 = try encodeAlloc(allocator, &di2); 627 | // defer allocator.free(cbor2); 628 | // try std.testing.expectEqualSlices(u8, &.{ 0xa2, 0x01, 0x02, 0x03, 0x04 }, cbor2); 629 | } 630 | 631 | test "MT6: encode cbor tagged data item 1(1363896240)" { 632 | const allocator = std.testing.allocator; 633 | 634 | var di = try DataItem.tagged(allocator, 1, DataItem.int(1363896240)); 635 | defer di.deinit(allocator); 636 | const cbor = try encodeAlloc(allocator, &di); 637 | defer allocator.free(cbor); 638 | try std.testing.expectEqualSlices(u8, &.{ 0xc1, 0x1a, 0x51, 0x4b, 0x67, 0xb0 }, cbor); 639 | } 640 | 641 | test "MT6: encode cbor tagged data item 32(\"http://www.example.com\")" { 642 | const allocator = std.testing.allocator; 643 | 644 | var di = try DataItem.tagged(allocator, 32, try DataItem.text("http://www.example.com", .{ .allocator = allocator })); 645 | defer di.deinit(allocator); 646 | const cbor = try encodeAlloc(allocator, &di); 647 | defer allocator.free(cbor); 648 | try std.testing.expectEqualSlices(u8, &.{ 0xd8, 0x20, 0x76, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d }, cbor); 649 | } 650 | 651 | test "MT7: encode f16 0.0" { 652 | const allocator = std.testing.allocator; 653 | 654 | var di = DataItem{ .float = Float{ .float16 = 0.0 } }; 655 | const cbor = try encodeAlloc(allocator, &di); 656 | defer allocator.free(cbor); 657 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x00, 0x00 }, cbor); 658 | } 659 | 660 | test "MT7: encode f16 -0.0" { 661 | const allocator = std.testing.allocator; 662 | 663 | var di = DataItem{ .float = Float{ .float16 = -0.0 } }; 664 | const cbor = try encodeAlloc(allocator, &di); 665 | defer allocator.free(cbor); 666 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x80, 0x00 }, cbor); 667 | } 668 | 669 | test "MT7: encode f16 1.0" { 670 | const allocator = std.testing.allocator; 671 | 672 | var di = DataItem{ .float = Float{ .float16 = 1.0 } }; 673 | const cbor = try encodeAlloc(allocator, &di); 674 | defer allocator.free(cbor); 675 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x3c, 0x00 }, cbor); 676 | } 677 | 678 | test "MT7: encode f16 1.5" { 679 | const allocator = std.testing.allocator; 680 | 681 | var di = DataItem{ .float = Float{ .float16 = 1.5 } }; 682 | const cbor = try encodeAlloc(allocator, &di); 683 | defer allocator.free(cbor); 684 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x3e, 0x00 }, cbor); 685 | } 686 | 687 | test "MT7: encode f16 5.960464477539063e-8" { 688 | const allocator = std.testing.allocator; 689 | 690 | var di = DataItem{ .float = Float{ .float16 = 5.960464477539063e-8 } }; 691 | const cbor = try encodeAlloc(allocator, &di); 692 | defer allocator.free(cbor); 693 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x00, 0x01 }, cbor); 694 | } 695 | 696 | test "MT7: encode f16 0.00006103515625" { 697 | const allocator = std.testing.allocator; 698 | 699 | var di = DataItem{ .float = Float{ .float16 = 0.00006103515625 } }; 700 | const cbor = try encodeAlloc(allocator, &di); 701 | defer allocator.free(cbor); 702 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0x04, 0x00 }, cbor); 703 | } 704 | 705 | test "MT7: encode f16 -4.0" { 706 | const allocator = std.testing.allocator; 707 | 708 | var di = DataItem{ .float = Float{ .float16 = -4.0 } }; 709 | const cbor = try encodeAlloc(allocator, &di); 710 | defer allocator.free(cbor); 711 | try std.testing.expectEqualSlices(u8, &.{ 0xf9, 0xc4, 0x00 }, cbor); 712 | } 713 | 714 | test "MT7: encode f32 100000.0" { 715 | const allocator = std.testing.allocator; 716 | 717 | var di = DataItem{ .float = Float{ .float32 = 100000.0 } }; 718 | const cbor = try encodeAlloc(allocator, &di); 719 | defer allocator.free(cbor); 720 | try std.testing.expectEqualSlices(u8, &.{ 0xfa, 0x47, 0xc3, 0x50, 0x00 }, cbor); 721 | } 722 | 723 | test "MT7: encode f32 3.4028234663852886e+38" { 724 | const allocator = std.testing.allocator; 725 | 726 | var di = DataItem{ .float = Float{ .float32 = 3.4028234663852886e+38 } }; 727 | const cbor = try encodeAlloc(allocator, &di); 728 | defer allocator.free(cbor); 729 | try std.testing.expectEqualSlices(u8, &.{ 0xfa, 0x7f, 0x7f, 0xff, 0xff }, cbor); 730 | } 731 | 732 | test "MT7: encode f64 1.1" { 733 | const allocator = std.testing.allocator; 734 | 735 | var di = DataItem{ .float = Float{ .float64 = 1.1 } }; 736 | const cbor = try encodeAlloc(allocator, &di); 737 | defer allocator.free(cbor); 738 | try std.testing.expectEqualSlices(u8, &.{ 0xfb, 0x3f, 0xf1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a }, cbor); 739 | } 740 | 741 | test "MT7: encode f64 1.0e+300" { 742 | const allocator = std.testing.allocator; 743 | 744 | var di = DataItem{ .float = Float{ .float64 = 1.0e+300 } }; 745 | const cbor = try encodeAlloc(allocator, &di); 746 | defer allocator.free(cbor); 747 | try std.testing.expectEqualSlices(u8, &.{ 0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c }, cbor); 748 | } 749 | 750 | test "MT7: encode f64 -4.1" { 751 | const allocator = std.testing.allocator; 752 | 753 | var di = DataItem{ .float = Float{ .float64 = -4.1 } }; 754 | const cbor = try encodeAlloc(allocator, &di); 755 | defer allocator.free(cbor); 756 | try std.testing.expectEqualSlices(u8, &.{ 0xfb, 0xc0, 0x10, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66 }, cbor); 757 | } 758 | 759 | test "MT7: encode simple values" { 760 | const allocator = std.testing.allocator; 761 | 762 | var di1 = DataItem.True(); 763 | const cbor1 = try encodeAlloc(allocator, &di1); 764 | defer allocator.free(cbor1); 765 | try std.testing.expectEqualSlices(u8, &.{0xf5}, cbor1); 766 | 767 | var di2 = DataItem.False(); 768 | const cbor2 = try encodeAlloc(allocator, &di2); 769 | defer allocator.free(cbor2); 770 | try std.testing.expectEqualSlices(u8, &.{0xf4}, cbor2); 771 | 772 | var di3 = DataItem.Null(); 773 | const cbor3 = try encodeAlloc(allocator, &di3); 774 | defer allocator.free(cbor3); 775 | try std.testing.expectEqualSlices(u8, &.{0xf6}, cbor3); 776 | 777 | var di4 = DataItem.Undefined(); 778 | const cbor4 = try encodeAlloc(allocator, &di4); 779 | defer allocator.free(cbor4); 780 | try std.testing.expectEqualSlices(u8, &.{0xf7}, cbor4); 781 | } 782 | 783 | test "MT0,1: DataItem{ .int = 30 } to json" { 784 | const allocator = std.testing.allocator; 785 | 786 | const di = DataItem{ .int = 30 }; 787 | 788 | var string = std.ArrayList(u8).init(allocator); 789 | defer string.deinit(); 790 | try std.json.stringify(di, .{}, string.writer()); 791 | 792 | try std.testing.expectEqualStrings("30", string.items); 793 | } 794 | 795 | test "MT2: DataItem to json" { 796 | const allocator = std.testing.allocator; 797 | 798 | const di = try DataItem.bytes(&.{ 0x95, 0x28, 0xe0, 0x8f, 0x32, 0xda, 0x3d, 0x36, 0x83, 0xc4, 0x6a, 0x1c, 0x36, 0x58, 0xb4, 0x86, 0x47, 0x2b }, .{ .allocator = allocator }); 799 | defer di.deinit(allocator); 800 | 801 | var string = std.ArrayList(u8).init(allocator); 802 | defer string.deinit(); 803 | try std.json.stringify(di, .{}, string.writer()); 804 | 805 | try std.testing.expectEqualStrings("\"lSjgjzLaPTaDxGocNli0hkcr\"", string.items); 806 | } 807 | 808 | test "MT3: DataItem to json" { 809 | const allocator = std.testing.allocator; 810 | 811 | const di = try DataItem.text("fido-u2f", .{ .allocator = allocator }); 812 | defer di.deinit(allocator); 813 | 814 | var string = std.ArrayList(u8).init(allocator); 815 | defer string.deinit(); 816 | try std.json.stringify(di, .{}, string.writer()); 817 | 818 | try std.testing.expectEqualStrings("\"fido-u2f\"", string.items); 819 | } 820 | 821 | test "MT4: DataItem to json" { 822 | const allocator = std.testing.allocator; 823 | 824 | const di = try DataItem.array(&.{ DataItem.int(1), DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator }); 825 | defer di.deinit(allocator); 826 | 827 | var string = std.ArrayList(u8).init(allocator); 828 | defer string.deinit(); 829 | try std.json.stringify(di, .{}, string.writer()); 830 | 831 | try std.testing.expectEqualStrings("[1,2,3]", string.items); 832 | } 833 | 834 | test "MT5: DataItem to json" { 835 | const allocator = std.testing.allocator; 836 | 837 | const di = try DataItem.map(&.{ Pair.new(try DataItem.text("a", .{ .allocator = allocator }), DataItem.int(1)), Pair.new(try DataItem.text("b", .{ .allocator = allocator }), try DataItem.array(&.{ DataItem.int(2), DataItem.int(3) }, .{ .allocator = allocator })) }, .{ .allocator = allocator }); 838 | defer di.deinit(allocator); 839 | 840 | var string = std.ArrayList(u8).init(allocator); 841 | defer string.deinit(); 842 | try std.json.stringify(di, .{}, string.writer()); 843 | 844 | try std.testing.expectEqualStrings("{\"a\":1,\"b\":[2,3]}", string.items); 845 | } 846 | 847 | test "MT6: BigNum and other tagged values to json" { 848 | const allocator = std.testing.allocator; 849 | 850 | const di1 = try DataItem.unsignedBignum(allocator, &.{ 0xf6, 0x53, 0xd8, 0xf5, 0x55, 0x8b, 0xf2, 0x49, 0x1d, 0x90, 0x96, 0x13, 0x44, 0x8d, 0xd1, 0xd3 }); 851 | defer di1.deinit(allocator); 852 | const di2 = try DataItem.signedBignum(allocator, &.{ 0xf6, 0x53, 0xd8, 0xf5, 0x55, 0x8b, 0xf2, 0x49, 0x1d, 0x90, 0x96, 0x13, 0x44, 0x8d, 0xd1, 0xd3 }); 853 | defer di2.deinit(allocator); 854 | const di3 = try DataItem.tagged(allocator, 22, try DataItem.bytes(&.{ 0xf6, 0x53, 0xd8, 0xf5, 0x55, 0x8b, 0xf2, 0x49, 0x1d, 0x90, 0x96, 0x13, 0x44, 0x8d, 0xd1, 0xd3 }, .{ .allocator = allocator })); 855 | defer di3.deinit(allocator); 856 | const di4 = try DataItem.tagged(allocator, 23, try DataItem.bytes("abcd", .{ .allocator = allocator })); 857 | defer di4.deinit(allocator); 858 | 859 | const json1 = try di1.toJson(allocator); 860 | defer json1.deinit(); 861 | const json2 = try di2.toJson(allocator); 862 | defer json2.deinit(); 863 | const json3 = try di3.toJson(allocator); 864 | defer json3.deinit(); 865 | const json4 = try di4.toJson(allocator); 866 | defer json4.deinit(); 867 | 868 | try std.testing.expectEqualStrings("\"9lPY9VWL8kkdkJYTRI3R0w\"", json1.items); 869 | try std.testing.expectEqualStrings("\"~9lPY9VWL8kkdkJYTRI3R0w\"", json2.items); 870 | try std.testing.expectEqualStrings("\"9lPY9VWL8kkdkJYTRI3R0w==\"", json3.items); 871 | try std.testing.expectEqualStrings("\"61626364\"", json4.items); 872 | } 873 | 874 | test "MT7: DataItem to json (false, true, null)" { 875 | const allocator = std.testing.allocator; 876 | 877 | const di1 = DataItem.False(); 878 | const di2 = DataItem.True(); 879 | const di3 = DataItem.Null(); 880 | const di4 = DataItem.Undefined(); 881 | 882 | const json1 = try di1.toJson(allocator); 883 | defer json1.deinit(); 884 | const json2 = try di2.toJson(allocator); 885 | defer json2.deinit(); 886 | const json3 = try di3.toJson(allocator); 887 | defer json3.deinit(); 888 | const json4 = try di4.toJson(allocator); 889 | defer json4.deinit(); 890 | 891 | try std.testing.expectEqualStrings("false", json1.items); 892 | try std.testing.expectEqualStrings("true", json2.items); 893 | try std.testing.expectEqualStrings("null", json3.items); 894 | // Any other simple value is represented as the substitue value (null). 895 | try std.testing.expectEqualStrings("null", json4.items); 896 | } 897 | 898 | test "MT7: DataItem to json (float)" { 899 | const allocator = std.testing.allocator; 900 | 901 | const di1 = DataItem.float64(-4.1); 902 | //const di2 = DataItem.float32(3.4028234663852886e+38); 903 | 904 | const json1 = try di1.toJson(allocator); 905 | defer json1.deinit(); 906 | //const json2 = try di2.toJson(allocator); 907 | //defer json2.deinit(); 908 | 909 | try std.testing.expectEqualStrings("-4.1e+00", json1.items); 910 | //try std.testing.expectEqualStrings("3.4028234663852886e+38", json2.items); 911 | } 912 | 913 | test "serialize WebAuthn attestationObject to json" { 914 | const allocator = std.testing.allocator; 915 | const attestationObject = try std.fs.cwd().openFile("data/WebAuthnCreate.dat", .{ .mode = .read_only }); 916 | defer attestationObject.close(); 917 | const bytes = try attestationObject.readToEndAlloc(allocator, 4096); 918 | defer allocator.free(bytes); 919 | 920 | var di = try decode(allocator, bytes); 921 | defer di.deinit(allocator); 922 | 923 | var json = std.ArrayList(u8).init(allocator); 924 | defer json.deinit(); 925 | try std.json.stringify(di, .{}, json.writer()); 926 | 927 | const expected = "{\"fmt\":\"fido-u2f\",\"attStmt\":{\"sig\":\"MEUCIQDxiq8pf_27Z2osKh-3EnKViLVnMvh5oSuUhhC1AtBb1wIgT-C4h13JDnutnjn1mR9JVfRlE0rXXoknYH5eI3jAqWc\",\"x5c\":[\"MIICvDCCAaSgAwIBAgIEA63wEjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgNjE3MzA4MzQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQZnoecFi233DnuSkKgRhalswn-ygkvdr4JSPltbpXK5MxlzVSgWc-9x8mzGysdbBhEecLAYfQYqpVLWWosHPoXo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNzATBgsrBgEEAYLlHAIBAQQEAwIEMDAhBgsrBgEEAYLlHAEBBAQSBBD6K5ncnjlCV4-SSjDSPEEYMAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBACjrs2f-0djw4onryp_22AdXxg6a5XyxcoybHDjKu72E2SN9qDGsIZSfDy38DDFr_bF1s25joiu7WA6tylKA0HmEDloeJXJiWjv7h2Az2_siqWnJOLic4XE1lAChJS2XAqkSk9VFGelg3SLOiifrBet-ebdQwAL-2QFrcR7JrXRQG9kUy76O2VcSgbdPROsHfOYeywarhalyVSZ-6OOYK_Q_DLIaOC0jXrnkzm2ymMQFQlBAIysrYeEM1wxiFbwDt-lAcbcOEtHEf5ZlWi75nUzlWn8bSx_5FO4TbZ5hIEcUiGRpiIBEMRZlOIm4ZIbZycn_vJOFRTVps0V0S4ygtDc\"]},\"authData\":\"IQkYX2k6AeoaJkH4LVL7ru4KT0fjN03--HCDjeSbDpdBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQLP4zbGAIJF2-iAaUW0bQvgCqA2vSNA3iCGm-91S3ha37_YiJXJDjeWFfnD57wWA6TfjAK7Q3_E_tqM-w4uBytClAQIDJiABIVgg2fTCo1ITbxnJqV2ogkq1zcTVYx68_VvbsL__JTYJEp4iWCDvQEuIB2VXYAeIij7Wq_-0JXtxI1UzJdRQYTy1vJo6Ug\"}"; 928 | 929 | try std.testing.expectEqualStrings(expected, json.items); 930 | } 931 | 932 | test "MT0,1: json to DataItem{ .int = 30 }" { 933 | const allocator = std.testing.allocator; 934 | 935 | const j = "30"; 936 | var s = std.json.TokenStream.init(j); 937 | const d = try DataItem.parseJson(allocator, &s); 938 | const e = DataItem{ .int = 30 }; 939 | try std.testing.expectEqual(e, d); 940 | } 941 | 942 | test "MT0,1: json to DataItem{ .int = 0 }" { 943 | const allocator = std.testing.allocator; 944 | 945 | const j = "0"; 946 | var s = std.json.TokenStream.init(j); 947 | const d = try DataItem.parseJson(allocator, &s); 948 | const e = DataItem{ .int = 0 }; 949 | try std.testing.expectEqual(e, d); 950 | } 951 | 952 | test "MT0,1: json to DataItem{ .int = 18446744073709551615 }" { 953 | const allocator = std.testing.allocator; 954 | 955 | const j = "18446744073709551615"; 956 | var s = std.json.TokenStream.init(j); 957 | const d = try DataItem.parseJson(allocator, &s); 958 | const e = DataItem{ .int = 18446744073709551615 }; 959 | try std.testing.expectEqual(e, d); 960 | } 961 | 962 | test "MT0,1: json to DataItem{ .int = -18446744073709551616 }" { 963 | const allocator = std.testing.allocator; 964 | 965 | const j = "-18446744073709551616"; 966 | var s = std.json.TokenStream.init(j); 967 | const d = try DataItem.parseJson(allocator, &s); 968 | const e = DataItem{ .int = -18446744073709551616 }; 969 | try std.testing.expectEqual(e, d); 970 | } 971 | 972 | test "MT3: json to text string 1" { 973 | const allocator = std.testing.allocator; 974 | 975 | const j = "\"IETF\""; 976 | var s = std.json.TokenStream.init(j); 977 | const d = try DataItem.parseJson(allocator, &s); 978 | defer d.deinit(allocator); 979 | const e = try DataItem.text("IETF", .{ .allocator = allocator }); 980 | defer e.deinit(allocator); 981 | try std.testing.expectEqualStrings(e.text, d.text); 982 | try std.testing.expect(e.equal(&d)); 983 | } 984 | 985 | test "MT3: json to text string 2" { 986 | const allocator = std.testing.allocator; 987 | 988 | const j = "\"\""; 989 | var s = std.json.TokenStream.init(j); 990 | const d = try DataItem.parseJson(allocator, &s); 991 | defer d.deinit(allocator); 992 | const e = try DataItem.text(&.{}, .{ .allocator = allocator }); 993 | defer e.deinit(allocator); 994 | try std.testing.expectEqualStrings(e.text, d.text); 995 | try std.testing.expect(e.equal(&d)); 996 | } 997 | 998 | test "MT3: json to text string 3" { 999 | const allocator = std.testing.allocator; 1000 | 1001 | const j = "\"a\""; 1002 | var s = std.json.TokenStream.init(j); 1003 | const d = try DataItem.parseJson(allocator, &s); 1004 | defer d.deinit(allocator); 1005 | const e = try DataItem.text("a", .{ .allocator = allocator }); 1006 | defer e.deinit(allocator); 1007 | try std.testing.expectEqualStrings(e.text, d.text); 1008 | try std.testing.expect(e.equal(&d)); 1009 | } 1010 | 1011 | test "MT6: bignum 2^64" { 1012 | const allocator = std.testing.allocator; 1013 | 1014 | const j = "18446744073709551616"; 1015 | var s = std.json.TokenStream.init(j); 1016 | const d = try DataItem.parseJson(allocator, &s); 1017 | defer d.deinit(allocator); 1018 | const e = try DataItem.tagged(allocator, 2, try DataItem.bytes(&.{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, .{ .allocator = allocator })); 1019 | defer e.deinit(allocator); 1020 | //try std.testing.expectEqual(e, d); 1021 | try std.testing.expect(d.isTagged()); 1022 | try std.testing.expectEqual(@as(u64, 2), d.tag.number); 1023 | try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, d.tag.content.bytes); 1024 | try std.testing.expect(e.equal(&d)); 1025 | } 1026 | 1027 | test "MT6: bignum 147573952589680980818" { 1028 | const allocator = std.testing.allocator; 1029 | 1030 | const j = "147573952589680980818"; 1031 | var s = std.json.TokenStream.init(j); 1032 | const d = try DataItem.parseJson(allocator, &s); 1033 | defer d.deinit(allocator); 1034 | const e = try DataItem.tagged(allocator, 2, try DataItem.bytes(&.{ 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0xB3, 0x52 }, .{ .allocator = allocator })); 1035 | defer e.deinit(allocator); 1036 | try std.testing.expect(e.equal(&d)); 1037 | } 1038 | 1039 | test "MT6: bignum -147573952589680980818" { 1040 | const allocator = std.testing.allocator; 1041 | 1042 | const j = "-147573952589680980818"; 1043 | var s = std.json.TokenStream.init(j); 1044 | const d = try DataItem.parseJson(allocator, &s); 1045 | defer d.deinit(allocator); 1046 | const e = try DataItem.tagged(allocator, 3, try DataItem.bytes(&.{ 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0xB3, 0x51 }, .{ .allocator = allocator })); 1047 | defer e.deinit(allocator); 1048 | try std.testing.expect(e.equal(&d)); 1049 | } 1050 | 1051 | test "MT7: json to f64 0.0" { 1052 | const allocator = std.testing.allocator; 1053 | 1054 | const j = "0.0"; 1055 | var s = std.json.TokenStream.init(j); 1056 | const d = try DataItem.parseJson(allocator, &s); 1057 | const e = DataItem.float64(0.0); 1058 | try std.testing.expectEqual(e, d); 1059 | } 1060 | 1061 | test "MT7: json to f64 100000.0" { 1062 | const allocator = std.testing.allocator; 1063 | 1064 | const j = "100000.0"; 1065 | var s = std.json.TokenStream.init(j); 1066 | const d = try DataItem.parseJson(allocator, &s); 1067 | const e = DataItem.float64(100000.0); 1068 | try std.testing.expectEqual(e, d); 1069 | } 1070 | 1071 | test "MT7: json to true" { 1072 | const allocator = std.testing.allocator; 1073 | 1074 | const j = "true"; 1075 | var s = std.json.TokenStream.init(j); 1076 | const d = try DataItem.parseJson(allocator, &s); 1077 | const e = DataItem.True(); 1078 | try std.testing.expectEqual(e, d); 1079 | } 1080 | 1081 | test "MT7: json to false" { 1082 | const allocator = std.testing.allocator; 1083 | 1084 | const j = "false"; 1085 | var s = std.json.TokenStream.init(j); 1086 | const d = try DataItem.parseJson(allocator, &s); 1087 | const e = DataItem.False(); 1088 | try std.testing.expectEqual(e, d); 1089 | } 1090 | 1091 | test "MT7: json to null" { 1092 | const allocator = std.testing.allocator; 1093 | 1094 | const j = "null"; 1095 | var s = std.json.TokenStream.init(j); 1096 | const d = try DataItem.parseJson(allocator, &s); 1097 | const e = DataItem.Null(); 1098 | try std.testing.expectEqual(e, d); 1099 | } 1100 | --------------------------------------------------------------------------------