1const std = @import("std");
2const testing = std.testing;
3
4const buffer = @import("buffer.zig");
5const codes = @import("codes.zig");
6const opts = @import("opts.zig");
7
8// CoAP version implemented by this library.
9//
10// From RFC 7252:
11//
12// Version (Ver): 2-bit unsigned integer. Indicates the CoAP version
13// number. Implementations of this specification MUST set this field
14// to 1 (01 binary).
15//
16const VERSION: u2 = 1;
17
18// 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 processed
23// as a message format error.
24//
25const MAX_TOKEN_LEN = 8;
26
27// 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 options
33// and the start of the payload.
34//
35const OPTION_END = 0xff;
36
37// CoAP message type.
38//
39// From RFC 7252:
40//
41// 2-bit unsigned integer. Indicates if this message is of type
42// Confirmable (0), Non-confirmable (1), Acknowledgement (2), or Reset
43// (3).
44//
45pub const Msg = enum(u2) {
46 con = 0, // Confirmable
47 non = 1, // Non-confirmable
48 ack = 2, // Acknowledgement
49 rst = 3, // Reset
50};
51
52pub const Header = packed struct {
53 token_len: u4,
54 type: Msg,
55 version: u2,
56 code: codes.Code,
57 message_id: u16,
58};
59
60/// Implements delta encoding for the CoAP option format.
61const DeltaEncoding = union(enum) {
62 noExt: u4,
63 extByte: u8,
64 extHalf: u16,
65
66 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(u4, val) };
73 },
74 13...268 => { // 268 = 2^8 + 13 - 1
75 // From RFC 7252:
76 //
77 // An 8-bit unsigned integer follows the initial byte and
78 // indicates the Option Delta minus 13.
79 return DeltaEncoding{ .extByte = @intCast(u8, (val - 13)) };
80 },
81 269...65804 => { // 65804 = 2^16 + 269 - 1
82 // From RFC 7252:
83 //
84 // A 16-bit unsigned integer in network byte order follows the
85 // initial byte and indicates the Option Delta minus 269.
86 const v = std.mem.nativeToBig(u16, @intCast(u16, val - 269));
87 return DeltaEncoding{ .extHalf = v };
88 },
89 else => unreachable,
90 }
91 }
92
93 /// 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 }
101
102 /// Amount of additionall extension bytes (0-2 bytes)
103 /// required to store this value (not including the initial ID
104 /// 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 }
112
113 /// Write extension bytes (0-2 bytes) to the given WriteBuffer
114 /// 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};
123
124pub const Response = struct {
125 header: Header,
126 token: []const u8,
127 buffer: buffer.WriteBuffer,
128 last_option: u32 = 0,
129 zero_payload: bool = true,
130
131 const WriteError = error{BufTooSmall};
132 const PayloadWriter = std.io.Writer(*Response, WriteError, write);
133
134 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;
139
140 var hdr = Header{
141 .version = VERSION,
142 .type = mt,
143 .token_len = @intCast(u4, token.len),
144 .code = code,
145 .message_id = id,
146 };
147
148 var r = Response{
149 .header = hdr,
150 .token = token,
151 .buffer = .{ .slice = buf },
152 };
153
154 hdr.message_id = std.mem.nativeToBig(u16, hdr.message_id);
155 const serialized = @bitCast(u32, hdr);
156
157 r.buffer.word(serialized);
158 r.buffer.bytes(token);
159
160 return r;
161 }
162
163 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 }
167
168 /// Add an option to the CoAP response. Options must be added in the
169 /// in order of their Option Numbers. After data has been written to
170 /// the payload, no additional options can be added. Both invariants
171 /// 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);
175
176 std.debug.assert(self.last_option <= opt.number);
177 const delta = opt.number - self.last_option;
178
179 const odelta = DeltaEncoding.encode(delta);
180 const olen = DeltaEncoding.encode(@intCast(u32, opt.value.len));
181
182 const reqcap = 1 + odelta.size() + olen.size() + opt.value.len;
183 if (self.buffer.capacity() < reqcap)
184 return error.BufTooSmall;
185
186 // See https://datatracker.ietf.org/doc/html/rfc7252#section-3.1
187 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);
191
192 self.last_option = opt.number;
193 }
194
195 /// Write data to the payload of the CoAP response. If the given data
196 /// 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;
201
202 // This function is part of the public API, thus safety-checked
203 // undefined behavior is not good enough and we add a bounds check.
204 if (self.buffer.capacity() < len)
205 return WriteError.BufTooSmall;
206
207 if (self.zero_payload) {
208 self.buffer.byte(OPTION_END);
209 self.zero_payload = false;
210 }
211
212 self.buffer.bytes(data);
213 return data.len; // Don't return len to not confuse caller.
214 }
215
216 /// 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(u8, code);
220 }
221
222 pub fn payloadWriter(self: *Response) PayloadWriter {
223 return PayloadWriter{ .context = self };
224 }
225
226 pub fn marshal(self: *Response) []u8 {
227 return self.buffer.serialized();
228 }
229};
230
231test "test header serialization" {
232 const exp = @embedFile("../testvectors/basic-header.bin");
233
234 var buf = [_]u8{0} ** exp.len;
235 var resp = try Response.init(&buf, Msg.con, codes.GET, &[_]u8{}, 2342);
236
237 const serialized = resp.marshal();
238 try testing.expect(std.mem.eql(u8, serialized, exp));
239}
240
241test "test setCode after packet creation" {
242 const exp = @embedFile("../testvectors/basic-header.bin");
243
244 var buf = [_]u8{0} ** exp.len;
245 var resp = try Response.init(&buf, Msg.con, codes.DELETE, &[_]u8{}, 2342);
246
247 // Change code from DELETE to GET. The latter is expected.
248 resp.setCode(codes.GET);
249
250 const serialized = resp.marshal();
251 try testing.expect(std.mem.eql(u8, serialized, exp));
252}
253
254test "test header serialization with token" {
255 const exp = @embedFile("../testvectors/with-token.bin");
256
257 var buf = [_]u8{0} ** exp.len;
258 var resp = try Response.init(&buf, Msg.ack, codes.PUT, &[_]u8{ 23, 42 }, 5);
259
260 const serialized = resp.marshal();
261 try testing.expect(std.mem.eql(u8, serialized, exp));
262}
263
264test "test header serialization with insufficient buffer space" {
265 const exp: []const u8 = &[_]u8{ 0, 0, 0 };
266 var buf = [_]u8{0} ** exp.len;
267
268 // Given buffer is large enough to contain header, but one byte too
269 // 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));
271
272 // Ensure that Response.init has no side effects.
273 try testing.expect(std.mem.eql(u8, &buf, exp));
274}
275
276test "test payload serialization" {
277 const exp = @embedFile("../testvectors/with-payload.bin");
278
279 var buf = [_]u8{0} ** exp.len;
280 var resp = try Response.init(&buf, Msg.rst, codes.DELETE, &[_]u8{}, 1);
281
282 var w = resp.payloadWriter();
283 try w.print("Hello", .{});
284
285 const serialized = resp.marshal();
286 try testing.expect(std.mem.eql(u8, serialized, exp));
287}
288
289test "test option serialization" {
290 const exp = @embedFile("../testvectors/with-options.bin");
291
292 var buf = [_]u8{0} ** exp.len;
293 var resp = try Response.init(&buf, Msg.con, codes.GET, &[_]u8{}, 2342);
294
295 // Zero byte extension
296 const opt0 = opts.Option{ .number = 2, .value = &[_]u8{0xff} };
297 try resp.addOption(&opt0);
298
299 // One byte extension
300 const opt1 = opts.Option{ .number = 23, .value = &[_]u8{ 13, 37 } };
301 try resp.addOption(&opt1);
302
303 // Two byte extension
304 const opt2 = opts.Option{ .number = 65535, .value = &[_]u8{} };
305 try resp.addOption(&opt2);
306
307 // 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));
310
311 const serialized = resp.marshal();
312 try testing.expect(std.mem.eql(u8, serialized, exp));
313}
314
315pub const Request = struct {
316 header: Header,
317 slice: buffer.ReadBuffer,
318 token: []const u8,
319 payload: ?([]const u8),
320 last_option: ?opts.Option,
321
322 pub fn init(buf: []const u8) !Request {
323 var slice = buffer.ReadBuffer{ .slice = buf };
324 if (buf.len < @sizeOf(Header))
325 return error.FormatError;
326
327 // Cast first four bytes to u32 and convert them to header struct
328 const serialized: u32 = try slice.word();
329 var hdr = @bitCast(Header, serialized);
330
331 // Convert message_id to a integer in host byteorder
332 hdr.message_id = std.mem.bigToNative(u16, hdr.message_id);
333
334 var token: []const u8 = &[_]u8{};
335 if (hdr.token_len > 0) {
336 if (hdr.token_len > MAX_TOKEN_LEN)
337 return error.FormatError;
338
339 token = slice.bytes(hdr.token_len) catch {
340 return error.FormatError;
341 };
342 }
343
344 // For the first instance in a message, a preceding
345 // option instance with Option Number zero is assumed.
346 const init_option = opts.Option{ .number = 0, .value = &[_]u8{} };
347
348 return Request{
349 .header = hdr,
350 .token = token,
351 .slice = slice,
352 .payload = null,
353 .last_option = init_option,
354 };
355 }
356
357 // https://datatracker.ietf.org/doc/html/rfc7252#section-3.1
358 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 and
364 // 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 the
375 // 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 }
395
396 /// Returns the next option or null if the packet contains a payload
397 /// and the option end has been reached. If the packet does not
398 /// 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;
404
405 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 }
418
419 const delta = try self.decodeValue(@intCast(u4, option >> 4));
420 const len = try self.decodeValue(@intCast(u4, option & 0xf));
421
422 var optnum = self.last_option.?.number + delta;
423 var optval = self.slice.bytes(len) catch {
424 return error.FormatError;
425 };
426
427 const ret = opts.Option{
428 .number = optnum,
429 .value = optval,
430 };
431
432 self.last_option = ret;
433 return ret;
434 }
435
436 /// 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 does
438 /// not exist. After this function has been called (even if an error
439 /// was returned) it is no longer possible to retrieve options with
440 /// a smaller Option Number then the given one. Similarly, when
441 /// attempting to find multiple options, this function must be
442 /// 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;
448
449 const n = self.last_option.?.number;
450 if (n > 0 and n >= optnum)
451 return error.InvalidArgument; // XXX: Use assert instead?
452
453 while (true) {
454 const next = try self.nextOption();
455 if (next == null)
456 return error.EndOfOptions;
457
458 const opt = next.?;
459 if (opt.number == optnum) {
460 return opt;
461 } else if (opt.number > optnum) {
462 return error.OptionNotFound;
463 }
464 }
465 }
466
467 /// Skip all remaining options in the CoAP packet and return a pointer
468 /// to the packet payload (if any). After this function has been
469 /// called it is no longer possible to extract options from the packet.
470 pub fn extractPayload(self: *Request) !(?[]const u8) {
471 while (true) {
472 var 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 }
481
482 std.debug.assert(self.last_option == null);
483 return self.payload;
484 }
485};
486
487test "test header parser" {
488 const buf = @embedFile("../testvectors/with-token.bin");
489 const req = try Request.init(buf);
490 const hdr = req.header;
491
492 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}
500
501test "test payload parsing" {
502 const buf = @embedFile("../testvectors/with-payload.bin");
503 var req = try Request.init(buf);
504
505 const payload = try req.extractPayload();
506 try testing.expect(std.mem.eql(u8, payload.?, "Hello"));
507}
508
509test "test nextOption without payload" {
510 const buf = @embedFile("../testvectors/with-options.bin");
511 var req = try Request.init(buf);
512
513 const opt1_opt = try req.nextOption();
514 const opt1 = opt1_opt.?;
515
516 try testing.expect(opt1.number == 2);
517 try testing.expect(std.mem.eql(u8, opt1.value, &[_]u8{0xff}));
518
519 const opt2_opt = try req.nextOption();
520 const opt2 = opt2_opt.?;
521
522 try testing.expect(opt2.number == 23);
523 try testing.expect(std.mem.eql(u8, opt2.value, &[_]u8{ 13, 37 }));
524
525 const opt3_opt = try req.nextOption();
526 const opt3 = opt3_opt.?;
527
528 try testing.expect(opt3.number == 65535);
529 try testing.expect(std.mem.eql(u8, opt3.value, &[_]u8{}));
530
531 // No payload marker → expect error.
532 try testing.expectError(error.EndOfStream, req.nextOption());
533}
534
535test "test nextOption with payload" {
536 const buf = @embedFile("../testvectors/payload-and-options.bin");
537 var req = try Request.init(buf);
538
539 const next_opt = try req.nextOption();
540 const opt = next_opt.?;
541
542 try testing.expect(opt.number == 0);
543 try testing.expect(std.mem.eql(u8, opt.value, "test"));
544
545 // Payload marker → expect null.
546 const last_opt = try req.nextOption();
547 try testing.expect(last_opt == null);
548
549 // Running nextOption again must return null again.
550 const last_opt_again = try req.nextOption();
551 try testing.expect(last_opt_again == null);
552
553 // Extracting payload must work.
554 const payload = try req.extractPayload();
555 try testing.expect(std.mem.eql(u8, payload.?, "foobar"));
556}
557
558test "test findOption without payload" {
559 const buf = @embedFile("../testvectors/with-options.bin");
560 var req = try Request.init(buf);
561
562 // First option
563 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));
567
568 // Third option, skipping second
569 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));
573
574 // Attempting to access the second option should result in usage error
575 try testing.expectError(error.InvalidArgument, req.findOption(23));
576
577 // Skipping options and accessing payload should work
578 // but return an error since this packet has no payload.
579 try testing.expectError(error.ZeroLengthPayload, req.extractPayload());
580}