zoap

A WiP CoAP implementation for bare-metal constrained devices in Zig

git clone https://git.8pit.net/zoap.git

  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}