From edf230b651f293a34116def6b1ad64bb0d91f8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20Vikstr=C3=B6m?= Date: Sun, 8 Mar 2026 01:37:14 +0100 Subject: [PATCH 1/3] Fix security/correctness issues from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _ to Code enum (prevent UB on unknown code bytes) - Add version validation (reject non-v1 per RFC 7252 §3) - Fix u16 overflow in readVarLen for 2-byte extended values - Widen delta_sum to u32 to prevent accumulation overflow - Add UnsortedOptions check in write() - Accept explicit allocator in write() for flexibility - Remove unused Big import, rename Self to Packet - Rename readVarLenDirect/writeExtDirect → readVarLen/writeExtended - Rename _data_buf → data_buf - Use explicit Error return type on read/write - Update README error table and encoding example --- README.md | 4 +- src/bench.zig | 4 +- src/root.zig | 133 ++++++++++++++++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 98e52c7..83ca0e6 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ defer pkt.deinit(); ### Encoding ```zig -const encoded = try pkt.write(); +const encoded = try pkt.write(allocator); defer allocator.free(encoded); // encoded is a freshly allocated []u8 containing the CoAP wire format ``` @@ -67,9 +67,11 @@ defer allocator.free(encoded); | Error | Condition | |----------------------|-------------------------------------------------| | `MessageTooShort` | Data shorter than 4-byte header or declared TKL | +| `InvalidVersion` | Version field is not 1 (per RFC 7252 §3) | | `InvalidTokenLength` | TKL field is 9--15 (reserved per RFC 7252) | | `TruncatedOption` | Option delta/length nibble 15, or truncated extended bytes / value | | `EmptyPayload` | Payload marker `0xFF` with no bytes following | +| `UnsortedOptions` | Options not in ascending order (encoding only) | ## Build diff --git a/src/bench.zig b/src/bench.zig index 941e0e6..dcf9bc7 100644 --- a/src/bench.zig +++ b/src/bench.zig @@ -107,7 +107,7 @@ fn encodeBench(comptime msg: []const u8) fn () void { _ = arena.reset(.retain_capacity); const alloc = arena.allocator(); const pkt = Packet.read(alloc, msg) catch unreachable; - const encoded = pkt.write() catch unreachable; + const encoded = pkt.write(alloc) catch unreachable; doNotOptimize(encoded); } }.run; @@ -120,7 +120,7 @@ fn roundtripBench(comptime msg: []const u8) fn () void { _ = arena.reset(.retain_capacity); const alloc = arena.allocator(); const pkt = Packet.read(alloc, msg) catch unreachable; - const encoded = pkt.write() catch unreachable; + const encoded = pkt.write(alloc) catch unreachable; const pkt2 = Packet.read(alloc, encoded) catch unreachable; doNotOptimize(pkt2); } diff --git a/src/root.zig b/src/root.zig index 71b0528..289f6e4 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,7 +1,6 @@ //! Constrained Application Protocol (CoAP) encode/decode library //! const std = @import("std"); -const Big = std.builtin.Endian.big; /// CoAP message type (CON, NON, ACK, RST). pub const MessageKind = enum(u8) { @@ -48,6 +47,7 @@ pub const Code = enum(u8) { gateway_timeout, proxying_not_supported, hop_limit_reached = 168, + _, }; /// CoAP option number. Supports all options from RFC 7252, 7641, 7959, and 8516. @@ -83,33 +83,35 @@ pub const Option = struct { value: []const u8, }; -/// Errors returned when decoding a malformed CoAP packet. +/// Errors returned when decoding/encoding a malformed CoAP packet. pub const Error = error{ MessageTooShort, + InvalidVersion, InvalidTokenLength, TruncatedOption, EmptyPayload, + UnsortedOptions, + OutOfMemory, }; /// Decoded CoAP packet. Owns its token, option values, and payload through /// a single backing buffer. Call `deinit()` to free. pub const Packet = struct { - const Self = @This(); - kind: MessageKind, code: Code, msg_id: u16, token: []const u8, options: []Option, payload: []const u8, - _data_buf: []u8, + data_buf: []u8, alloc: std.mem.Allocator, /// Decode a CoAP packet from raw bytes. Returns `Error` on malformed input. - pub fn read(alloc: std.mem.Allocator, data: []const u8) !Packet { + pub fn read(alloc: std.mem.Allocator, data: []const u8) Error!Packet { if (data.len < 4) return Error.MessageTooShort; const b0 = data[0]; + if (b0 >> 6 != 1) return Error.InvalidVersion; const b1 = data[1]; const msg_id: u16 = @as(u16, data[2]) << 8 | data[3]; const token_len: usize = b0 & 0xf; @@ -131,8 +133,8 @@ pub const Packet = struct { data_size += data.len - pos; break; } - _ = try readVarLenDirect(data, &pos, @intCast(c0 >> 4 & 0xf)); - const val_len = try readVarLenDirect(data, &pos, @intCast(c0 & 0xf)); + _ = try readVarLen(data, &pos, @intCast(c0 >> 4 & 0xf)); + const val_len = try readVarLen(data, &pos, @intCast(c0 & 0xf)); if (pos + val_len > data.len) return Error.TruncatedOption; data_size += val_len; pos += val_len; @@ -140,9 +142,9 @@ pub const Packet = struct { } // Pass 2: allocate and populate - const data_buf = try alloc.alloc(u8, data_size); + const data_buf = alloc.alloc(u8, data_size) catch return Error.OutOfMemory; errdefer alloc.free(data_buf); - const options = try alloc.alloc(Option, opt_count); + const options = alloc.alloc(Option, opt_count) catch return Error.OutOfMemory; errdefer alloc.free(options); // Copy token @@ -153,15 +155,15 @@ pub const Packet = struct { // Parse and copy options pos = 4 + token_len; - var delta_sum: u16 = 0; + var delta_sum: u32 = 0; for (options) |*opt| { const c0 = data[pos]; pos += 1; - delta_sum += try readVarLenDirect(data, &pos, @intCast(c0 >> 4 & 0xf)); - const val_len = try readVarLenDirect(data, &pos, @intCast(c0 & 0xf)); + delta_sum += try readVarLen(data, &pos, @intCast(c0 >> 4 & 0xf)); + const val_len = try readVarLen(data, &pos, @intCast(c0 & 0xf)); @memcpy(data_buf[buf_pos .. buf_pos + val_len], data[pos .. pos + val_len]); opt.* = .{ - .kind = @enumFromInt(delta_sum), + .kind = @enumFromInt(@as(u16, @intCast(delta_sum))), .value = data_buf[buf_pos .. buf_pos + val_len], }; buf_pos += val_len; @@ -184,17 +186,18 @@ pub const Packet = struct { .token = token, .options = options, .payload = payload, - ._data_buf = data_buf, + .data_buf = data_buf, }; } /// Encode the packet to CoAP wire format. Caller owns the returned slice. - pub fn write(self: Self) ![]u8 { + pub fn write(self: Packet, allocator: std.mem.Allocator) Error![]u8 { // Calculate exact output size var size: usize = 4 + self.token.len; var prev: u16 = 0; for (self.options) |opt| { const num = @intFromEnum(opt.kind); + if (num < prev) return Error.UnsortedOptions; const delta = num - prev; const len: u16 = @intCast(opt.value.len); size += 1 + extendedSize(delta) + extendedSize(len) + opt.value.len; @@ -205,8 +208,8 @@ pub const Packet = struct { } // Single allocation - const buf = try self.alloc.alloc(u8, size); - errdefer self.alloc.free(buf); + const buf = allocator.alloc(u8, size) catch return Error.OutOfMemory; + errdefer allocator.free(buf); // Header const token_len: u8 = @intCast(self.token.len); @@ -228,8 +231,8 @@ pub const Packet = struct { const len: u16 = @intCast(opt.value.len); buf[pos] = (optNibble(delta) << 4) | optNibble(len); pos += 1; - writeExtDirect(buf, &pos, delta); - writeExtDirect(buf, &pos, len); + writeExtended(buf, &pos, delta); + writeExtended(buf, &pos, len); @memcpy(buf[pos .. pos + opt.value.len], opt.value); pos += opt.value.len; prev = num; @@ -246,8 +249,8 @@ pub const Packet = struct { } /// Free the backing buffer and options array. - pub fn deinit(self: Self) void { - self.alloc.free(self._data_buf); + pub fn deinit(self: Packet) void { + self.alloc.free(self.data_buf); self.alloc.free(self.options); } }; @@ -258,7 +261,7 @@ fn optNibble(val: u16) u8 { return 14; } -fn writeExtDirect(buf: []u8, pos: *usize, val: u16) void { +fn writeExtended(buf: []u8, pos: *usize, val: u16) void { switch (optNibble(val)) { 13 => { buf[pos.*] = @intCast(val - 13); @@ -282,7 +285,7 @@ fn extendedSize(val: u16) usize { }; } -fn readVarLenDirect(data: []const u8, pos: *usize, nibble: u4) !u16 { +fn readVarLen(data: []const u8, pos: *usize, nibble: u4) Error!u16 { return switch (nibble) { 13 => blk: { if (pos.* >= data.len) return Error.TruncatedOption; @@ -294,7 +297,7 @@ fn readVarLenDirect(data: []const u8, pos: *usize, nibble: u4) !u16 { if (pos.* + 1 >= data.len) return Error.TruncatedOption; const v: u16 = @as(u16, data[pos.*]) << 8 | data[pos.* + 1]; pos.* += 2; - break :blk v + 269; + break :blk std.math.add(u16, v, 269) catch return Error.TruncatedOption; }, 15 => Error.TruncatedOption, else => nibble, @@ -370,7 +373,7 @@ test "decode assertions" { for ([_][]const u8{ &msg1, &msg2, &msg3, &msg4 }) |msg| { const pkt = try Packet.read(alloc, msg); defer pkt.deinit(); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try expectEqualSlices(u8, msg, enc); } @@ -399,7 +402,7 @@ test "all message kinds" { const pkt = try Packet.read(alloc, &msg); defer pkt.deinit(); try std.testing.expectEqual(kind, pkt.kind); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -413,7 +416,7 @@ test "message id boundaries" { const pkt = try Packet.read(alloc, &msg); defer pkt.deinit(); try std.testing.expectEqual(id, pkt.msg_id); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -433,7 +436,7 @@ test "token lengths" { const pkt = try Packet.read(alloc, msg); defer pkt.deinit(); try std.testing.expectEqual(@as(usize, tl), pkt.token.len); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, msg, enc); } @@ -451,7 +454,7 @@ test "response codes" { const pkt = try Packet.read(alloc, &msg); defer pkt.deinit(); try std.testing.expectEqual(code, pkt.code); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -491,7 +494,7 @@ test "option length boundaries" { defer pkt.deinit(); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(usize, opt_len), pkt.options[0].value.len); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, buf, enc); } @@ -527,7 +530,7 @@ test "option delta boundaries" { defer pkt.deinit(); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(u16, delta), @intFromEnum(pkt.options[0].kind)); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, buf, enc); } @@ -556,7 +559,7 @@ test "extended delta and length combined" { try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(u16, 300), @intFromEnum(pkt.options[0].kind)); try std.testing.expectEqual(@as(usize, 300), pkt.options[0].value.len); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -570,7 +573,7 @@ test "zero-length option values" { try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(OptionKind.if_none_match, pkt.options[0].kind); try std.testing.expectEqual(@as(usize, 0), pkt.options[0].value.len); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -591,7 +594,7 @@ test "multiple options with accumulating deltas" { try std.testing.expectEqualSlices(u8, "a", pkt.options[0].value); try std.testing.expectEqualSlices(u8, "b", pkt.options[1].value); try std.testing.expectEqualSlices(u8, "c", pkt.options[2].value); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -613,7 +616,7 @@ test "option kinds" { const delta_nibble = optNibble(delta); buf_arr[pos] = (delta_nibble << 4) | 0; pos += 1; - writeExtDirect(&buf_arr, &pos, delta); + writeExtended(&buf_arr, &pos, delta); prev = num; } const msg = buf_arr[0..pos]; @@ -624,7 +627,7 @@ test "option kinds" { for (pkt.options, 0..) |opt, i| { try std.testing.expectEqual(@as(u16, kind_nums[i]), @intFromEnum(opt.kind)); } - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, msg, enc); } @@ -659,7 +662,7 @@ test "payload edge cases" { for ([_][]const u8{ &msg_no_payload, &msg_1byte, &msg_opt_payload }) |msg| { const pkt = try Packet.read(alloc, msg); defer pkt.deinit(); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, msg, enc); } @@ -677,7 +680,7 @@ test "rfc7641 observe option" { try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(OptionKind.observe, pkt.options[0].kind); try std.testing.expectEqualSlices(u8, &[_]u8{0x01}, pkt.options[0].value); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -698,7 +701,7 @@ test "rfc7959 block options" { try std.testing.expectEqual(OptionKind.block2, pkt.options[0].kind); try std.testing.expectEqual(OptionKind.block1, pkt.options[1].kind); try std.testing.expectEqual(OptionKind.size2, pkt.options[2].kind); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -712,7 +715,7 @@ test "rfc8516 too many requests" { try std.testing.expectEqual(.acknowledgement, pkt.kind); try std.testing.expectEqual(.too_many_requests, pkt.code); try std.testing.expectEqual(@as(u16, 1), pkt.msg_id); - const enc = try pkt.write(); + const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, &msg, enc); } @@ -734,7 +737,7 @@ test "round-trip" { for (messages) |msg| { const pkt = try Packet.read(alloc, msg); defer pkt.deinit(); - const encoded = try pkt.write(); + const encoded = try pkt.write(alloc); defer alloc.free(encoded); try std.testing.expectEqualSlices(u8, msg, encoded); } @@ -759,7 +762,7 @@ test "round-trip with extended option delta" { try std.testing.expectEqual(.no_response, pkt.options[0].kind); try std.testing.expectEqualSlices(u8, &[_]u8{0x02}, pkt.options[0].value); - const encoded = try pkt.write(); + const encoded = try pkt.write(alloc); defer alloc.free(encoded); try std.testing.expectEqualSlices(u8, &msg, encoded); } @@ -831,3 +834,47 @@ test "error: empty payload after options" { // Option then payload marker with no bytes following try std.testing.expectError(Error.EmptyPayload, Packet.read(alloc, &[_]u8{ 0x40, 0x01, 0x00, 0x00, 0xB0, 0xFF })); } + +test "error: invalid version" { + const alloc = std.testing.allocator; + // Version 0 (0x00 in top 2 bits) + try std.testing.expectError(Error.InvalidVersion, Packet.read(alloc, &[_]u8{ 0x00, 0x01, 0x00, 0x00 })); + // Version 2 (0x80 in top 2 bits) + try std.testing.expectError(Error.InvalidVersion, Packet.read(alloc, &[_]u8{ 0x80, 0x01, 0x00, 0x00 })); + // Version 3 (0xC0 in top 2 bits) + try std.testing.expectError(Error.InvalidVersion, Packet.read(alloc, &[_]u8{ 0xC0, 0x01, 0x00, 0x00 })); +} + +test "unknown response code round-trip" { + const alloc = std.testing.allocator; + // Code byte 0x08 is unassigned but valid on the wire + const msg = [_]u8{ 0x40, 0x08, 0x00, 0x00 }; + const pkt = try Packet.read(alloc, &msg); + defer pkt.deinit(); + try std.testing.expectEqual(@as(u8, 0x08), @intFromEnum(pkt.code)); + const enc = try pkt.write(alloc); + defer alloc.free(enc); + try std.testing.expectEqualSlices(u8, &msg, enc); +} + +test "error: unsorted options in write" { + const alloc = std.testing.allocator; + // Construct a packet with unsorted options + const data_buf = try alloc.alloc(u8, 0); + defer alloc.free(data_buf); + var opts = [_]Option{ + .{ .kind = .uri_path, .value = &.{} }, + .{ .kind = .uri_host, .value = &.{} }, // uri_host(3) < uri_path(11) + }; + const pkt = Packet{ + .alloc = alloc, + .kind = .confirmable, + .code = .get, + .msg_id = 0, + .token = &.{}, + .options = &opts, + .payload = &.{}, + .data_buf = data_buf, + }; + try std.testing.expectError(Error.UnsortedOptions, pkt.write(alloc)); +} From 14c81c317f3a1f7adca8ba8fff1adc7b6af1c89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20Vikstr=C3=B6m?= Date: Sun, 8 Mar 2026 02:03:31 +0100 Subject: [PATCH 2/3] Remove stored allocator from Packet struct deinit() now takes explicit allocator parameter, matching write(). Saves 16 bytes per Packet, fully explicit API. --- README.md | 2 +- src/root.zig | 60 ++++++++++++++++++++++++---------------------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 83ca0e6..7a9fe5e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ const data = [_]u8{ }; const pkt = try coap.Packet.read(allocator, &data); -defer pkt.deinit(); +defer pkt.deinit(allocator); // pkt.kind == .confirmable // pkt.code == .get diff --git a/src/root.zig b/src/root.zig index 289f6e4..20b7ecb 100644 --- a/src/root.zig +++ b/src/root.zig @@ -105,8 +105,6 @@ pub const Packet = struct { payload: []const u8, data_buf: []u8, - alloc: std.mem.Allocator, - /// Decode a CoAP packet from raw bytes. Returns `Error` on malformed input. pub fn read(alloc: std.mem.Allocator, data: []const u8) Error!Packet { if (data.len < 4) return Error.MessageTooShort; @@ -179,7 +177,6 @@ pub const Packet = struct { } return .{ - .alloc = alloc, .kind = @enumFromInt(b0 >> 4 & 0x3), .code = @enumFromInt(b1), .msg_id = msg_id, @@ -249,9 +246,9 @@ pub const Packet = struct { } /// Free the backing buffer and options array. - pub fn deinit(self: Packet) void { - self.alloc.free(self.data_buf); - self.alloc.free(self.options); + pub fn deinit(self: Packet, allocator: std.mem.Allocator) void { + allocator.free(self.data_buf); + allocator.free(self.options); } }; @@ -316,7 +313,7 @@ test "decode assertions" { 0x63, }; const p1 = try Packet.read(alloc, &msg1); - defer p1.deinit(); + defer p1.deinit(alloc); try expectEqual(.confirmable, p1.kind); try expectEqual(.get, p1.code); try expectEqual(@as(u16, 0xba22), p1.msg_id); @@ -333,7 +330,7 @@ test "decode assertions" { 0x61, 0x74, 0x61, }; const p2 = try Packet.read(alloc, &msg2); - defer p2.deinit(); + defer p2.deinit(alloc); try expectEqual(.confirmable, p2.kind); try expectEqual(.post, p2.code); try expectEqual(@as(u16, 0x3e6f), p2.msg_id); @@ -347,7 +344,7 @@ test "decode assertions" { 0x73, 0x74, 0x83, 0x74, 0x76, 0x31, }; const p3 = try Packet.read(alloc, &msg3); - defer p3.deinit(); + defer p3.deinit(alloc); try expectEqual(.confirmable, p3.kind); try expectEqual(@as(usize, 2), p3.options.len); try expectEqual(OptionKind.uri_host, p3.options[0].kind); @@ -363,7 +360,7 @@ test "decode assertions" { 0x6F, 0x72, 0x6C, 0x64, 0x21, }; const p4 = try Packet.read(alloc, &msg4); - defer p4.deinit(); + defer p4.deinit(alloc); try expectEqual(.acknowledgement, p4.kind); try expectEqual(.content, p4.code); try expectEqual(@as(usize, 0), p4.options.len); @@ -372,7 +369,7 @@ test "decode assertions" { // Round-trip all four for ([_][]const u8{ &msg1, &msg2, &msg3, &msg4 }) |msg| { const pkt = try Packet.read(alloc, msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); const enc = try pkt.write(alloc); defer alloc.free(enc); try expectEqualSlices(u8, msg, enc); @@ -400,7 +397,7 @@ test "all message kinds" { var msg = [_]u8{ 0x40, 0x01, 0x00, 0x00 }; msg[0] = (1 << 6) | (@as(u8, @intCast(i)) << 4); const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(kind, pkt.kind); const enc = try pkt.write(alloc); defer alloc.free(enc); @@ -414,7 +411,7 @@ test "message id boundaries" { for (ids) |id| { const msg = [_]u8{ 0x40, 0x01, @intCast(id >> 8), @intCast(id & 0xff) }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(id, pkt.msg_id); const enc = try pkt.write(alloc); defer alloc.free(enc); @@ -434,7 +431,7 @@ test "token lengths" { for (0..tl) |j| buf[4 + j] = @intCast(j + 0xA0); const msg = buf[0 .. 4 + tl]; const pkt = try Packet.read(alloc, msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, tl), pkt.token.len); const enc = try pkt.write(alloc); defer alloc.free(enc); @@ -452,7 +449,7 @@ test "response codes" { for (codes) |code| { const msg = [_]u8{ 0x40, @intFromEnum(code), 0x00, 0x00 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(code, pkt.code); const enc = try pkt.write(alloc); defer alloc.free(enc); @@ -491,7 +488,7 @@ test "option length boundaries" { for (0..opt_len) |j| buf[pos + j] = @intCast(j & 0xff); const pkt = try Packet.read(alloc, buf); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(usize, opt_len), pkt.options[0].value.len); const enc = try pkt.write(alloc); @@ -527,7 +524,7 @@ test "option delta boundaries" { } const pkt = try Packet.read(alloc, buf); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(u16, delta), @intFromEnum(pkt.options[0].kind)); const enc = try pkt.write(alloc); @@ -555,7 +552,7 @@ test "extended delta and length combined" { for (0..300) |j| msg[9 + j] = @intCast(j & 0xff); const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(@as(u16, 300), @intFromEnum(pkt.options[0].kind)); try std.testing.expectEqual(@as(usize, 300), pkt.options[0].value.len); @@ -569,7 +566,7 @@ test "zero-length option values" { // if_none_match (5) with zero-length value const msg = [_]u8{ 0x40, 0x01, 0x00, 0x00, 0x50 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(OptionKind.if_none_match, pkt.options[0].kind); try std.testing.expectEqual(@as(usize, 0), pkt.options[0].value.len); @@ -588,7 +585,7 @@ test "multiple options with accumulating deltas" { 0x01, 0x63, // delta=0 (uri_path again), len=1, "c" }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 3), pkt.options.len); for (pkt.options) |opt| try std.testing.expectEqual(OptionKind.uri_path, opt.kind); try std.testing.expectEqualSlices(u8, "a", pkt.options[0].value); @@ -622,7 +619,7 @@ test "option kinds" { const msg = buf_arr[0..pos]; const pkt = try Packet.read(alloc, msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 21), pkt.options.len); for (pkt.options, 0..) |opt, i| { try std.testing.expectEqual(@as(u16, kind_nums[i]), @intFromEnum(opt.kind)); @@ -638,13 +635,13 @@ test "payload edge cases" { // No payload const msg_no_payload = [_]u8{ 0x40, 0x01, 0x00, 0x00 }; const pkt1 = try Packet.read(alloc, &msg_no_payload); - defer pkt1.deinit(); + defer pkt1.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), pkt1.payload.len); // 1-byte payload const msg_1byte = [_]u8{ 0x40, 0x01, 0x00, 0x00, 0xFF, 0x42 }; const pkt2 = try Packet.read(alloc, &msg_1byte); - defer pkt2.deinit(); + defer pkt2.deinit(alloc); try std.testing.expectEqualSlices(u8, &[_]u8{0x42}, pkt2.payload); // Payload after options @@ -654,14 +651,14 @@ test "payload edge cases" { 0xFF, 0xAA, 0xBB, }; const pkt3 = try Packet.read(alloc, &msg_opt_payload); - defer pkt3.deinit(); + defer pkt3.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt3.options.len); try std.testing.expectEqualSlices(u8, &[_]u8{ 0xAA, 0xBB }, pkt3.payload); // Round-trip all for ([_][]const u8{ &msg_no_payload, &msg_1byte, &msg_opt_payload }) |msg| { const pkt = try Packet.read(alloc, msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); const enc = try pkt.write(alloc); defer alloc.free(enc); try std.testing.expectEqualSlices(u8, msg, enc); @@ -676,7 +673,7 @@ test "rfc7641 observe option" { 0x61, 0x01, // option: delta=6 (observe), len=1, value=0x01 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), pkt.options.len); try std.testing.expectEqual(OptionKind.observe, pkt.options[0].kind); try std.testing.expectEqualSlices(u8, &[_]u8{0x01}, pkt.options[0].value); @@ -696,7 +693,7 @@ test "rfc7959 block options" { 0x12, 0x01, 0x00, // delta=1 (->28 Size2), len=2, value=0x0100 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(usize, 3), pkt.options.len); try std.testing.expectEqual(OptionKind.block2, pkt.options[0].kind); try std.testing.expectEqual(OptionKind.block1, pkt.options[1].kind); @@ -711,7 +708,7 @@ test "rfc8516 too many requests" { // ACK 4.29 Too Many Requests const msg = [_]u8{ 0x60, 0x9D, 0x00, 0x01 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(.acknowledgement, pkt.kind); try std.testing.expectEqual(.too_many_requests, pkt.code); try std.testing.expectEqual(@as(u16, 1), pkt.msg_id); @@ -736,7 +733,7 @@ test "round-trip" { for (messages) |msg| { const pkt = try Packet.read(alloc, msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); const encoded = try pkt.write(alloc); defer alloc.free(encoded); try std.testing.expectEqualSlices(u8, msg, encoded); @@ -752,7 +749,7 @@ test "round-trip with extended option delta" { const msg = [_]u8{ 0x40, 0x01, 0x00, 0x01, 0xD1, 0xF5, 0x02 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(.confirmable, pkt.kind); try std.testing.expectEqual(.get, pkt.code); @@ -850,7 +847,7 @@ test "unknown response code round-trip" { // Code byte 0x08 is unassigned but valid on the wire const msg = [_]u8{ 0x40, 0x08, 0x00, 0x00 }; const pkt = try Packet.read(alloc, &msg); - defer pkt.deinit(); + defer pkt.deinit(alloc); try std.testing.expectEqual(@as(u8, 0x08), @intFromEnum(pkt.code)); const enc = try pkt.write(alloc); defer alloc.free(enc); @@ -867,7 +864,6 @@ test "error: unsorted options in write" { .{ .kind = .uri_host, .value = &.{} }, // uri_host(3) < uri_path(11) }; const pkt = Packet{ - .alloc = alloc, .kind = .confirmable, .code = .get, .msg_id = 0, From d957b57959b823439b6768ad1c53f5306d9d0793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20Vikstr=C3=B6m?= Date: Sun, 8 Mar 2026 02:04:43 +0100 Subject: [PATCH 3/3] Update benchmark results in README --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7a9fe5e..8cb3cb5 100644 --- a/README.md +++ b/README.md @@ -89,28 +89,28 @@ AMD Ryzen AI MAX+ 395, 32 threads, 112 GiB RAM, Linux 6.18.9 ``` Decode: - minimal (4B header only) 26 ns/op 37546680 ops/s - small (token + 1 option) 90 ns/op 10989830 ops/s - multi-option (3 opts + payload) 190 ns/op 5260000 ops/s - host+path (2 options) 138 ns/op 7198226 ops/s - payload (ACK + 12B body) 38 ns/op 26128314 ops/s - extended delta (opt 258) 86 ns/op 11540017 ops/s + minimal (4B header only) 16 ns/op 62154070 ops/s + small (token + 1 option) 35 ns/op 28215709 ops/s + multi-option (3 opts + payload) 66 ns/op 15066762 ops/s + host+path (2 options) 50 ns/op 19986943 ops/s + payload (ACK + 12B body) 25 ns/op 38955629 ops/s + extended delta (opt 258) 34 ns/op 29074014 ops/s Encode (read + write): - minimal (4B header only) 32 ns/op 30992027 ops/s - small (token + 1 option) 106 ns/op 9370659 ops/s - multi-option (3 opts + payload) 216 ns/op 4628666 ops/s - host+path (2 options) 159 ns/op 6265900 ops/s - payload (ACK + 12B body) 46 ns/op 21709709 ops/s - extended delta (opt 258) 104 ns/op 9615095 ops/s + minimal (4B header only) 16 ns/op 61655540 ops/s + small (token + 1 option) 39 ns/op 25337057 ops/s + multi-option (3 opts + payload) 72 ns/op 13819127 ops/s + host+path (2 options) 56 ns/op 17744944 ops/s + payload (ACK + 12B body) 25 ns/op 39308928 ops/s + extended delta (opt 258) 38 ns/op 26017315 ops/s Round-trip (decode + encode + decode): - minimal (4B header only) 58 ns/op 17071762 ops/s - small (token + 1 option) 195 ns/op 5105325 ops/s - multi-option (3 opts + payload) 404 ns/op 2471514 ops/s - host+path (2 options) 296 ns/op 3370795 ops/s - payload (ACK + 12B body) 76 ns/op 13025843 ops/s - extended delta (opt 258) 188 ns/op 5294927 ops/s + minimal (4B header only) 28 ns/op 35168467 ops/s + small (token + 1 option) 72 ns/op 13789785 ops/s + multi-option (3 opts + payload) 135 ns/op 7378439 ops/s + host+path (2 options) 105 ns/op 9464411 ops/s + payload (ACK + 12B body) 48 ns/op 20817496 ops/s + extended delta (opt 258) 70 ns/op 14200029 ops/s ``` Benchmarks use `ArenaAllocator` with retained capacity (reset per iteration, no syscalls after warmup).