1const std = @import("std");2const testing = std.testing;34const buffer = @import("buffer.zig");5const codes = @import("codes.zig");6const opts = @import("opts.zig");78// CoAP version implemented by this library.9//10// From RFC 7252:11//12// Version (Ver): 2-bit unsigned integer. Indicates the CoAP version13// number. Implementations of this specification MUST set this field14// to 1 (01 binary).15//16const VERSION: u2 = 1;1718// Maximum length of a CoAP token.19//20// From RFC 7252:21//22// Lengths 9-15 are reserved, MUST NOT be sent, and MUST be processed23// as a message format error.24//25const MAX_TOKEN_LEN = 8;2627// CoAP Payload marker.28//29// From RFC 7252:30//31// If present and of non-zero length, it is prefixed by a fixed,32// one-byte Payload Marker (0xFF), which indicates the end of options33// and the start of the payload.34//35const OPTION_END = 0xff;3637// CoAP message type.38//39// From RFC 7252:40//41// 2-bit unsigned integer. Indicates if this message is of type42// Confirmable (0), Non-confirmable (1), Acknowledgement (2), or Reset43// (3).44//45pub const Msg = enum(u2) {46 con = 0, // Confirmable47 non = 1, // Non-confirmable48 ack = 2, // Acknowledgement49 rst = 3, // Reset50};5152pub const Header = packed struct {53 token_len: u4,54 type: Msg,55 version: u2,56 code: codes.Code,57 message_id: u16,58};5960/// Implements delta encoding for the CoAP option format.61const DeltaEncoding = union(enum) {62 noExt: u4,63 extByte: u8,64 extHalf: u16,6566 fn encode(val: u32) DeltaEncoding {67 switch (val) {68 0...12 => {69 // From RFC 7252:70 //71 // A value between 0 and 12 indicates the Option Delta.72 return DeltaEncoding{ .noExt = @intCast(val) };73 },74 13...268 => { // 268 = 2^8 + 13 - 175 // From RFC 7252:76 //77 // An 8-bit unsigned integer follows the initial byte and78 // indicates the Option Delta minus 13.79 return DeltaEncoding{ .extByte = @intCast(val - 13) };80 },81 269...65804 => { // 65804 = 2^16 + 269 - 182 // From RFC 7252:83 //84 // A 16-bit unsigned integer in network byte order follows the85 // initial byte and indicates the Option Delta minus 269.86 const v = std.mem.nativeToBig(u16, @intCast(val - 269));87 return DeltaEncoding{ .extHalf = v };88 },89 else => unreachable,90 }91 }9293 /// Identifier for nibbles in the first CoAP option byte.94 fn id(self: DeltaEncoding) u4 {95 switch (self) {96 DeltaEncoding.noExt => |x| return x,97 DeltaEncoding.extByte => return 13,98 DeltaEncoding.extHalf => return 14,99 }100 }101102 /// Amount of additionall extension bytes (0-2 bytes)103 /// required to store this value (not including the initial ID104 /// byte in the option format).105 fn size(self: DeltaEncoding) usize {106 return switch (self) {107 DeltaEncoding.noExt => 0,108 DeltaEncoding.extByte => 1,109 DeltaEncoding.extHalf => 2,110 };111 }112113 /// Write extension bytes (0-2 bytes) to the given WriteBuffer114 /// with safety-checked undefined behaviour.115 fn writeExtend(self: DeltaEncoding, wb: *buffer.WriteBuffer) void {116 switch (self) {117 DeltaEncoding.noExt => {},118 DeltaEncoding.extByte => |x| wb.byte(x),119 DeltaEncoding.extHalf => |x| wb.half(x),120 }121 }122};123124pub const Response = struct {125 header: Header,126 token: []const u8,127 buffer: buffer.WriteBuffer,128 last_option: u32 = 0,129 zero_payload: bool = true,130131 const WriteError = error{BufTooSmall};132 const PayloadWriter = std.io.Writer(*Response, WriteError, write);133134 pub fn init(buf: []u8, mt: Msg, code: codes.Code, token: []const u8, id: u16) !Response {135 if (buf.len < @sizeOf(Header) + token.len)136 return error.BufTooSmall;137 if (token.len > MAX_TOKEN_LEN)138 return error.InvalidTokenLength;139140 var hdr = Header{141 .version = VERSION,142 .type = mt,143 .token_len = @intCast(token.len),144 .code = code,145 .message_id = id,146 };147148 var r = Response{149 .header = hdr,150 .token = token,151 .buffer = .{ .slice = buf },152 };153154 hdr.message_id = std.mem.nativeToBig(u16, hdr.message_id);155 const serialized = @as(u32, @bitCast(hdr));156157 r.buffer.word(serialized);158 r.buffer.bytes(token);159160 return r;161 }162163 pub fn reply(buf: []u8, req: *const Request, mt: Msg, code: codes.Code) !Response {164 const hdr = req.header;165 return init(buf, mt, code, req.token, hdr.message_id);166 }167168 /// Add an option to the CoAP response. Options must be added in the169 /// in order of their Option Numbers. After data has been written to170 /// the payload, no additional options can be added. Both invariants171 /// are enforced using assertions in Debug and ReleaseSafe modes.172 pub fn addOption(self: *Response, opt: *const opts.Option) !void {173 // This function cannot be called after payload has been written.174 std.debug.assert(self.zero_payload);175176 std.debug.assert(self.last_option <= opt.number);177 const delta = opt.number - self.last_option;178179 const odelta = DeltaEncoding.encode(delta);180 const olen = DeltaEncoding.encode(@intCast(opt.value.len));181182 const reqcap = 1 + odelta.size() + olen.size() + opt.value.len;183 if (self.buffer.capacity() < reqcap)184 return error.BufTooSmall;185186 // See https://datatracker.ietf.org/doc/html/rfc7252#section-3.1187 self.buffer.byte(@as(u8, odelta.id()) << 4 | olen.id());188 odelta.writeExtend(&self.buffer);189 olen.writeExtend(&self.buffer);190 self.buffer.bytes(opt.value);191192 self.last_option = opt.number;193 }194195 /// Write data to the payload of the CoAP response. If the given data196 /// exceeds the available space in the buffer, an error is returned.197 fn write(self: *Response, data: []const u8) WriteError!usize {198 var len = data.len;199 if (self.zero_payload)200 len += 1;201202 // This function is part of the public API, thus safety-checked203 // undefined behavior is not good enough and we add a bounds check.204 if (self.buffer.capacity() < len)205 return WriteError.BufTooSmall;206207 if (self.zero_payload) {208 self.buffer.byte(OPTION_END);209 self.zero_payload = false;210 }211212 self.buffer.bytes(data);213 return data.len; // Don't return len to not confuse caller.214 }215216 /// Update CoAP response code after creating the packet.217 pub fn setCode(self: *Response, code: codes.Code) void {218 // Code is *always* the second byte in the buffer.219 self.buffer.slice[1] = @bitCast(code);220 }221222 pub fn payloadWriter(self: *Response) PayloadWriter {223 return PayloadWriter{ .context = self };224 }225226 pub fn marshal(self: *Response) []u8 {227 return self.buffer.serialized();228 }229};230231test "test header serialization" {232 const exp = @embedFile("testvectors/basic-header.bin");233234 var buf = [_]u8{0} ** exp.len;235 var resp = try Response.init(&buf, Msg.con, codes.GET, &[_]u8{}, 2342);236237 const serialized = resp.marshal();238 try testing.expect(std.mem.eql(u8, serialized, exp));239}240241test "test setCode after packet creation" {242 const exp = @embedFile("testvectors/basic-header.bin");243244 var buf = [_]u8{0} ** exp.len;245 var resp = try Response.init(&buf, Msg.con, codes.DELETE, &[_]u8{}, 2342);246247 // Change code from DELETE to GET. The latter is expected.248 resp.setCode(codes.GET);249250 const serialized = resp.marshal();251 try testing.expect(std.mem.eql(u8, serialized, exp));252}253254test "test header serialization with token" {255 const exp = @embedFile("testvectors/with-token.bin");256257 var buf = [_]u8{0} ** exp.len;258 var resp = try Response.init(&buf, Msg.ack, codes.PUT, &[_]u8{ 23, 42 }, 5);259260 const serialized = resp.marshal();261 try testing.expect(std.mem.eql(u8, serialized, exp));262}263264test "test header serialization with insufficient buffer space" {265 const exp: []const u8 = &[_]u8{ 0, 0, 0 };266 var buf = [_]u8{0} ** exp.len;267268 // Given buffer is large enough to contain header, but one byte too269 // small too contain the given token, thus an error should be raised.270 try testing.expectError(error.BufTooSmall, Response.init(&buf, Msg.ack, codes.PUT, &[_]u8{23}, 5));271272 // Ensure that Response.init has no side effects.273 try testing.expect(std.mem.eql(u8, &buf, exp));274}275276test "test payload serialization" {277 const exp = @embedFile("testvectors/with-payload.bin");278279 var buf = [_]u8{0} ** exp.len;280 var resp = try Response.init(&buf, Msg.rst, codes.DELETE, &[_]u8{}, 1);281282 var w = resp.payloadWriter();283 try w.print("Hello", .{});284285 const serialized = resp.marshal();286 try testing.expect(std.mem.eql(u8, serialized, exp));287}288289test "test option serialization" {290 const exp = @embedFile("testvectors/with-options.bin");291292 var buf = [_]u8{0} ** exp.len;293 var resp = try Response.init(&buf, Msg.con, codes.GET, &[_]u8{}, 2342);294295 // Zero byte extension296 const opt0 = opts.Option{ .number = 2, .value = &[_]u8{0xff} };297 try resp.addOption(&opt0);298299 // One byte extension300 const opt1 = opts.Option{ .number = 23, .value = &[_]u8{ 13, 37 } };301 try resp.addOption(&opt1);302303 // Two byte extension304 const opt2 = opts.Option{ .number = 65535, .value = &[_]u8{} };305 try resp.addOption(&opt2);306307 // Two byte extension (not enough space in buffer)308 const opt_err = opts.Option{ .number = 65535, .value = &[_]u8{} };309 try testing.expectError(error.BufTooSmall, resp.addOption(&opt_err));310311 const serialized = resp.marshal();312 try testing.expect(std.mem.eql(u8, serialized, exp));313}314315pub const Request = struct {316 header: Header,317 slice: buffer.ReadBuffer,318 token: []const u8,319 payload: ?([]const u8),320 last_option: ?opts.Option,321322 pub fn init(buf: []const u8) !Request {323 var slice = buffer.ReadBuffer{ .slice = buf };324 if (buf.len < @sizeOf(Header))325 return error.FormatError;326327 // Cast first four bytes to u32 and convert them to header struct328 const serialized: u32 = try slice.word();329 var hdr = @as(Header, @bitCast(serialized));330331 // Convert message_id to a integer in host byteorder332 hdr.message_id = std.mem.bigToNative(u16, hdr.message_id);333334 var token: []const u8 = &[_]u8{};335 if (hdr.token_len > 0) {336 if (hdr.token_len > MAX_TOKEN_LEN)337 return error.FormatError;338339 token = slice.bytes(hdr.token_len) catch {340 return error.FormatError;341 };342 }343344 // For the first instance in a message, a preceding345 // option instance with Option Number zero is assumed.346 const init_option = opts.Option{ .number = 0, .value = &[_]u8{} };347348 return Request{349 .header = hdr,350 .token = token,351 .slice = slice,352 .payload = null,353 .last_option = init_option,354 };355 }356357 // https://datatracker.ietf.org/doc/html/rfc7252#section-3.1358 fn decodeValue(self: *Request, val: u4) !u16 {359 switch (val) {360 13 => {361 // From RFC 7252:362 //363 // 13: An 8-bit unsigned integer follows the initial byte and364 // indicates the Option Delta minus 13.365 //366 const result = self.slice.byte() catch {367 return error.FormatError;368 };369 return @as(u16, result + 13);370 },371 14 => {372 // From RFC 7252:373 //374 // 14: A 16-bit unsigned integer in network byte order follows the375 // initial byte and indicates the Option Delta minus 269.376 //377 const result = self.slice.half() catch {378 return error.FormatError;379 };380 return std.mem.bigToNative(u16, result) + 269;381 },382 15 => {383 // From RFC 7252:384 //385 // 15: Reserved for future use. If the field is set to this value,386 // it MUST be processed as a message format error.387 //388 return error.PayloadMarker;389 },390 else => {391 return val;392 },393 }394 }395396 /// Returns the next option or null if the packet contains a payload397 /// and the option end has been reached. If the packet does not398 /// contain a payload an error is returned.399 ///400 /// Options are returned in the order of their Option Numbers.401 fn nextOption(self: *Request) !?opts.Option {402 if (self.last_option == null)403 return null;404405 const option = self.slice.byte() catch {406 return error.EndOfStream;407 };408 if (option == OPTION_END) {409 self.last_option = null;410 if (self.slice.length() < 1) {411 // For zero-length payload OPTION_END should not be set.412 return error.InvalidPayload;413 } else {414 self.payload = self.slice.remaining();415 }416 return null;417 }418419 const delta = try self.decodeValue(@intCast(option >> 4));420 const len = try self.decodeValue(@intCast(option & 0xf));421422 const optnum = self.last_option.?.number + delta;423 const optval = self.slice.bytes(len) catch {424 return error.FormatError;425 };426427 const ret = opts.Option{428 .number = optnum,429 .value = optval,430 };431432 self.last_option = ret;433 return ret;434 }435436 /// Find an option with the given Option Number in the CoAP packet.437 /// It is an error if an option with the given Option Number does438 /// not exist. After this function has been called (even if an error439 /// was returned) it is no longer possible to retrieve options with440 /// a smaller Option Number then the given one. Similarly, when441 /// attempting to find multiple options, this function must be442 /// called in order of their Option Numbers.443 pub fn findOption(self: *Request, optnum: u32) !opts.Option {444 if (optnum == 0)445 return error.InvalidArgument;446 if (self.last_option == null)447 return error.EndOfOptions;448449 const n = self.last_option.?.number;450 if (n > 0 and n >= optnum)451 return error.InvalidArgument; // XXX: Use assert instead?452453 while (true) {454 const next = try self.nextOption();455 if (next == null)456 return error.EndOfOptions;457458 const opt = next.?;459 if (opt.number == optnum) {460 return opt;461 } else if (opt.number > optnum) {462 return error.OptionNotFound;463 }464 }465 }466467 /// Skip all remaining options in the CoAP packet and return a pointer468 /// to the packet payload (if any). After this function has been469 /// called it is no longer possible to extract options from the packet.470 pub fn extractPayload(self: *Request) !(?[]const u8) {471 while (true) {472 const opt = self.nextOption() catch |err| {473 // The absence of the Payload Marker denotes a zero-length payload.474 if (err == error.EndOfStream)475 return error.ZeroLengthPayload;476 return err;477 };478 if (opt == null)479 break;480 }481482 std.debug.assert(self.last_option == null);483 return self.payload;484 }485};486487test "test header parser" {488 const buf = @embedFile("testvectors/with-token.bin");489 const req = try Request.init(buf);490 const hdr = req.header;491492 try testing.expect(hdr.version == VERSION);493 try testing.expect(hdr.type == Msg.ack);494 try testing.expect(hdr.token_len == 2);495 try testing.expect(req.token[0] == 23);496 try testing.expect(req.token[1] == 42);497 try testing.expect(hdr.code.equal(codes.PUT));498 try testing.expect(hdr.message_id == 5);499}500501test "test payload parsing" {502 const buf = @embedFile("testvectors/with-payload.bin");503 var req = try Request.init(buf);504505 const payload = try req.extractPayload();506 try testing.expect(std.mem.eql(u8, payload.?, "Hello"));507}508509test "test nextOption without payload" {510 const buf = @embedFile("testvectors/with-options.bin");511 var req = try Request.init(buf);512513 const opt1_opt = try req.nextOption();514 const opt1 = opt1_opt.?;515516 try testing.expect(opt1.number == 2);517 try testing.expect(std.mem.eql(u8, opt1.value, &[_]u8{0xff}));518519 const opt2_opt = try req.nextOption();520 const opt2 = opt2_opt.?;521522 try testing.expect(opt2.number == 23);523 try testing.expect(std.mem.eql(u8, opt2.value, &[_]u8{ 13, 37 }));524525 const opt3_opt = try req.nextOption();526 const opt3 = opt3_opt.?;527528 try testing.expect(opt3.number == 65535);529 try testing.expect(std.mem.eql(u8, opt3.value, &[_]u8{}));530531 // No payload marker → expect error.532 try testing.expectError(error.EndOfStream, req.nextOption());533}534535test "test nextOption with payload" {536 const buf = @embedFile("testvectors/payload-and-options.bin");537 var req = try Request.init(buf);538539 const next_opt = try req.nextOption();540 const opt = next_opt.?;541542 try testing.expect(opt.number == 0);543 try testing.expect(std.mem.eql(u8, opt.value, "test"));544545 // Payload marker → expect null.546 const last_opt = try req.nextOption();547 try testing.expect(last_opt == null);548549 // Running nextOption again must return null again.550 const last_opt_again = try req.nextOption();551 try testing.expect(last_opt_again == null);552553 // Extracting payload must work.554 const payload = try req.extractPayload();555 try testing.expect(std.mem.eql(u8, payload.?, "foobar"));556}557558test "test findOption without payload" {559 const buf = @embedFile("testvectors/with-options.bin");560 var req = try Request.init(buf);561562 // First option563 const opt1 = try req.findOption(2);564 try testing.expect(opt1.number == 2);565 const exp1: []const u8 = &[_]u8{0xff};566 try testing.expect(std.mem.eql(u8, exp1, opt1.value));567568 // Third option, skipping second569 const opt3 = try req.findOption(65535);570 try testing.expect(opt3.number == 65535);571 const exp3: []const u8 = &[_]u8{};572 try testing.expect(std.mem.eql(u8, exp3, opt3.value));573574 // Attempting to access the second option should result in usage error575 try testing.expectError(error.InvalidArgument, req.findOption(23));576577 // Skipping options and accessing payload should work578 // but return an error since this packet has no payload.579 try testing.expectError(error.ZeroLengthPayload, req.extractPayload());580}