Representing Non-empty Array

I’m implementing part of the CANopen protocol.

As part of the protocol you can transfer data of a size from 1 to 4 bytes.


pub const DataSetSize = enum(u2) {
    four_octets = 0x00,
    three_octets = 0x01,
    two_octets = 0x02,
    one_octet = 0x03,
};

pub const SDOClientExpedited = packed struct(u128) {
    sdo_header: SDOHeaderClient,
    data: u32,

    pub fn downloadInitiate(
        index: u16,
        subindex: u8,
        data: std.BoundedArray(u8, 4),
    ) !SDOClientExpedited {
        if (data.len == 0) {
            return error.InvalidParameterDataEmpty;
        }
        const size: DataSetSize = blk: {
            if (data.len == 1) {
                break :blk DataSetSize.one_octet;
            } else if (data.len == 2) {
                break :blk DataSetSize.two_octets;
            } else if (data.len == 3) {
                break :blk DataSetSize.three_octets;
            } else if (data.len == 4) {
                break :blk DataSetSize.four_octets;
            }
        };

        // data is not guaranteed to be zeroed
        var data_buf = std.mem.zeroes([4]u8);
        @memcpy(data_buf[0..data.len], data.slice());
        return SDOClientExpedited{
            .sdo_header = .{
                .size_indicator = true,
                .transfer_type = .expedited,
                .data_set_size = size,
                .command = .initiate_download_request,
                .index = index,
                .subindex = subindex,
            },
            .data = @bitCast(data_buf),
        };
    }
};

How do I fix the following issues I have with the above code:

  1. The function returns an error, but I think this can be expressed without errors.

Some constraints:

  1. I want to use a packed struct to assist serialization of this binary protocol. This means the data field of the struct cannot be an array.
  2. I want to provide this function because I want to express constraints on the possible combinations of values for the fields of the header. The header need to be setup correctly and I don’t want to remember details about these fields when using this struct later.

Maybe use slice an asserts?

pub const SDOClientExpedited = packed struct(u128) {
    sdo_header: SDOHeaderClient,
    data: u32,

    pub fn downloadInitiate(
        index: u16,
        subindex: u8,
        data: []const u8,
    ) SDOClientExpedited {
        assert(data.len != 0);
        assert(data.len < 5);

        const size: DataSetSize = blk: {
            if (data.len == 1) {
                break :blk DataSetSize.one_octet;
            } else if (data.len == 2) {
                break :blk DataSetSize.two_octets;
            } else if (data.len == 3) {
                break :blk DataSetSize.three_octets;
            } else {
                break :blk DataSetSize.four_octets;
            }
        };

        var data_buf = std.mem.zeroes([4]u8);
        @memcpy(data_buf[0..data.len], data);

        return SDOClientExpedited{
            .sdo_header = .{
                .size_indicator = true,
                .transfer_type = .expedited,
                .data_set_size = size,
                .command = .initiate_download_request,
                .index = index,
                .subindex = subindex,
            },
            .data = @bitCast(data_buf),
        };
    }
};

1 Like

You can assert the length with the BoundedArray approach. I think the BoundedArray is better.
If you are asserting the length, I’m assuming you have already validated elsewhere that it is not empty. If you haven’t, then use either an optional or an error.
Also, this part:

if (data.len == 0) {
            return error.InvalidParameterDataEmpty;
        }
        const size: DataSetSize = blk: {
            if (data.len == 1) {
                break :blk DataSetSize.one_octet;
            } else if (data.len == 2) {
                break :blk DataSetSize.two_octets;
            } else if (data.len == 3) {
                break :blk DataSetSize.three_octets;
            } else if (data.len == 4) {
                break :blk DataSetSize.four_octets;
            }
        };

Can be rewritten as:

const size = switch(data.len){
  0 => return error.InvalidParameterDataEmpty,
  1 => DataSetSize.one_octet,
  2 => DataSetSize.two_octets,
  3 => DataSetSize.three_octets,
  4 => DataSetSize.four_octets,
  else => unreachable,
}