Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tinyframe api #59

Merged
merged 2 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ SRCS := tests/cobs_encode_max_c.c \
tests/test_cobs_encode_max.cc \
tests/test_cobs_encode.cc \
tests/test_cobs_encode_inc.cc \
tests/test_cobs_encode_inplace.cc \
tests/test_cobs_encode_tinyframe.cc \
tests/test_cobs_decode.cc \
tests/test_cobs_decode_inc.cc \
tests/test_cobs_decode_inplace.cc \
tests/test_cobs_decode_tinyframe.cc \
tests/test_many_random_payloads.cc \
tests/test_paper_figures.cc \
tests/test_wikipedia.cc \
Expand Down
60 changes: 30 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ Finally, I didn't see as many unit tests as I'd have liked in the other librarie

## Metrics

It's pretty small, and you probably need either `cobs_[en|de]code_inplace` _or_ `cobs_[en|de]code[_inc*]`, but not both.
It's pretty small, and you probably need either `cobs_[en|de]code_tinyframe` _or_ `cobs_[en|de]code[_inc*]`, but not both.
```
❯ arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -Os -c cobs.c
❯ arm-none-eabi-nm --print-size --size-sort cobs.o

0000011c 0000001e T cobs_encode_inc_end (30 bytes)
0000007a 00000022 T cobs_encode_inc_begin (34 bytes)
00000048 00000032 T cobs_decode_inplace (50 bytes)
00000048 00000032 T cobs_decode_tinyframe (50 bytes)
0000013a 00000034 T cobs_encode (52 bytes)
00000000 00000048 T cobs_encode_inplace (72 bytes)
00000000 00000048 T cobs_encode_tinyframe (72 bytes)
0000009c 00000080 T cobs_encode_inc (128 bytes)
0000016e 00000090 T cobs_decode (144 bytes)
Total 1fe (510 bytes)
Expand Down Expand Up @@ -62,6 +62,26 @@ if (result == COBS_RET_SUCCESS) {
}
```

### Decoding

Decoding works similarly; receive an encoded buffer from somewhere, prepare a buffer to hold the decoded data, and call `cobs_decode`. Decoding can always be performed in-place, since the encoded frames are always larger than the decoded data. Simply pass the same buffer to the `encoded` and `decoded` parameters and the frame will be decoded in-place.

```
char encoded[128];
unsigned encoded_len;
get_encoded_data_from_somewhere(encoded, &encoded_len);

char decoded[128];
unsigned decoded_len;
cobs_ret_t const result = cobs_decode(encoded, encoded_len, decoded, sizeof(decoded), &decoded_len);

if (result == COBS_RET_SUCCESS) {
// decoding succeeded, 'decoded' and 'decoded_len' hold details.
} else {
// decoding failed, look to 'result' for details.
}
```

### Incremental Encoding

Sometimes it's helpful to be able to encode multiple separate buffers into one target. To do this, use the `cobs_encode_inc` family of functions: initialize a `cobx_enc_ctx_t` in `cobs_encode_inc_begin`, then call `cobs_encode_inc` multiple times, and finish encoding with `cobs_encode_inc_end`.
Expand Down Expand Up @@ -90,9 +110,9 @@ if (r != COBS_RET_SUCCESS) { /* handle your error, return / assert, whatever */
|encoded_len| contains the length of the encoded buffer. */
```

### Encoding In-Place
### Encoding "Tiny Frames"

The COBS protocol requires an extra byte at the beginning and end of the payload. If encoding and decoding in-place, it becomes your responsibility to reserve these extra bytes. It's easy to mess this up and just put your own data at byte 0, but your data must start at byte 1. For safety and sanity, `cobs_encode_inplace` will error with `COBS_RET_ERR_BAD_PAYLOAD` if the first and last bytes aren't explicitly set to the sentinel value. You have to put them there.
If you can guarantee that your payloads are shorter than 254 bytes, then you can use the "tinyframe" API, which lets you both decode and encode in-place in a single buffer. The COBS protocol requires an extra byte at the beginning and end of the payload. If encoding and decoding in-place, it becomes your responsibility to reserve these extra bytes. It's easy to mess this up and just put your own data at byte 0, but your data must start at byte 1. For safety and sanity, `cobs_encode_tinyframe` will error with `COBS_RET_ERR_BAD_PAYLOAD` if the first and last bytes aren't explicitly set to the sentinel value. You have to put them there.

(Note that `64` is an arbitrary size in this example, you can use any size you want up to `COBS_INPLACE_SAFE_BUFFER_SIZE`)

Expand All @@ -103,38 +123,17 @@ buf[63] = COBS_SENTINEL_VALUE; // You have to do this.

// Now, fill buf[1 .. 63] with whatever data you want.

cobs_ret_t const result = cobs_encode_inplace(buf, 64);
cobs_ret_t const result = cobs_encode_tinyframe(buf, 64);

if (result == COBS_RET_SUCCESS) {
// encoding succeeded, 'buf' now holds the encoded data.
} else {
// encoding failed, look to 'result' for details.
}
```
### Decoding With Separate Buffers
### Decoding "Tiny Frames"

Decoding works similarly; receive an encoded buffer from somewhere, prepare a buffer to hold the decoded data, and call `cobs_decode`.
```
char encoded[128];
unsigned encoded_len;
get_encoded_data_from_somewhere(encoded, &encoded_len);

char decoded[128];
unsigned decoded_len;
cobs_ret_t const result = cobs_decode(encoded, encoded_len, decoded, sizeof(decoded), &decoded_len);

if (result == COBS_RET_SUCCESS) {
// decoding succeeded, 'decoded' and 'decoded_len' hold details.
} else {
// decoding failed, look to 'result' for details.
}
```

### Decoding In-Place

There are two ways to decode COBS-encoded data in-place. One is simply by calling `cobs_decode` and pass your buffer to both the `enc` and `out_dec` parameters. Since COBS decoding never needs to revisit decoded bytes, and decoded payloads are always shorter than encoded payloads, this is guaranteed to work.

`cobs_decode_inplace` is also provided and offers byte-layout-parity to `cobs_encode_inplace`. This lets you, for example, decode a payload, change some bytes, and re-encode it all in the same buffer:
`cobs_decode_tinyframe` is also provided and offers byte-layout-parity to `cobs_encode_tinyframe`. This lets you, for example, decode a payload, change some bytes, and re-encode it all in the same buffer:

Accumulate data from your source until you encounter a COBS frame delimiter byte of `0x00`. Once you've got that, call `cobs_decode_inplace` on that region of a buffer to do an in-place decoding. The zeroth and final bytes of your payload will be replaced with the `COBS_SENTINEL_VALUE` bytes that, were you _encoding_ in-place, you would have had to place there anyway.

Expand All @@ -144,14 +143,15 @@ char buf[64];
// You fill 'buf' with an encoded cobs frame (from uart, etc) that ends with 0x00.
unsigned const length = you_fill_buf_with_data(buf);

cobs_ret_t const result = cobs_decode_inplace(buf, length);
cobs_ret_t const result = cobs_decode_tinyframe(buf, length);
if (result == COBS_RET_SUCCESS) {
// decoding succeeded, 'buf' bytes 0 and length-1 are COBS_SENTINEL_VALUE.
// your data is in 'buf[1 ... length-2]'
} else {
// decoding failed, look to 'result' for details.
}
```

## Developing

`nanocobs` uses [doctest](https://github.com/onqtam/doctest) for unit and functional testing; its unified mega-header is checked in to the `tests` directory. To build and run all tests on macOS or Linux, run `make -j` from a terminal. To build + run all tests on Windows, run the `vsvarsXX.bat` of your choice to set up the VS environment, then run `make-win.bat` (if you want to make that part better, pull requests are very welcome).
Expand Down
12 changes: 6 additions & 6 deletions cobs.c
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// SPDX-License-Identifier: Unlicense OR 0BSD
#include "cobs.h"

#define COBS_ISV COBS_INPLACE_SENTINEL_VALUE
#define COBS_TFSV COBS_TINYFRAME_SENTINEL_VALUE

typedef unsigned char cobs_byte_t;

cobs_ret_t cobs_encode_inplace(void *buf, size_t len) {
cobs_ret_t cobs_encode_tinyframe(void *buf, size_t len) {
if (!buf || (len < 2)) {
return COBS_RET_ERR_BAD_ARG;
}

cobs_byte_t *const src = (cobs_byte_t *)buf;
if ((src[0] != COBS_ISV) || (src[len - 1] != COBS_ISV)) {
if ((src[0] != COBS_TFSV) || (src[len - 1] != COBS_TFSV)) {
return COBS_RET_ERR_BAD_PAYLOAD;
}

Expand All @@ -36,7 +36,7 @@ cobs_ret_t cobs_encode_inplace(void *buf, size_t len) {
return COBS_RET_SUCCESS;
}

cobs_ret_t cobs_decode_inplace(void *buf, size_t const len) {
cobs_ret_t cobs_decode_tinyframe(void *buf, size_t const len) {
if (!buf || (len < 2)) {
return COBS_RET_ERR_BAD_ARG;
}
Expand All @@ -56,8 +56,8 @@ cobs_ret_t cobs_decode_inplace(void *buf, size_t const len) {
if (cur != len - 1) {
return COBS_RET_ERR_BAD_PAYLOAD;
}
src[0] = COBS_ISV;
src[len - 1] = COBS_ISV;
src[0] = COBS_TFSV;
src[len - 1] = COBS_TFSV;
return COBS_RET_SUCCESS;
}

Expand Down
10 changes: 5 additions & 5 deletions cobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum {
COBS_FRAME_DELIMITER = 0x00,

// In-place encoding mandatory placeholder byte values.
COBS_INPLACE_SENTINEL_VALUE = 0x5A,
COBS_TINYFRAME_SENTINEL_VALUE = 0x5A,

// In-place encodings that fit in a buffer of this size will always succeed.
COBS_INPLACE_SAFE_BUFFER_SIZE = 256
Expand All @@ -45,7 +45,7 @@ inline constexpr size_t COBS_ENCODE_MAX(size_t DECODED_LEN) {
(1 + (DECODED_LEN) + (((DECODED_LEN) + 253) / 254) + ((DECODED_LEN) == 0))
#endif

// cobs_encode_inplace
// cobs_encode_tinyframe
//
// Encode in-place the contents of the provided buffer |buf| of length |len|. Returns
// COBS_RET_SUCCESS on successful encoding.
Expand All @@ -64,9 +64,9 @@ inline constexpr size_t COBS_ENCODE_MAX(size_t DECODED_LEN) {
//
// If the function returns COBS_RET_ERR_BAD_PAYLOAD, the contents of |buf| are left
// indeterminate and must not be relied on to be fully encoded or decoded.
cobs_ret_t cobs_encode_inplace(void *buf, size_t len);
cobs_ret_t cobs_encode_tinyframe(void *buf, size_t len);

// cobs_decode_inplace
// cobs_decode_tinyframe
//
// Decode in-place the contents of the provided buffer |buf| of length |len|.
// Returns COBS_RET_SUCCESS on successful decoding.
Expand All @@ -84,7 +84,7 @@ cobs_ret_t cobs_encode_inplace(void *buf, size_t len);
//
// If the function returns COBS_RET_ERR_BAD_PAYLOAD, the contents of |buf| are left
// indeterminate and must not be relied on to be fully encoded or decoded.
cobs_ret_t cobs_decode_inplace(void *buf, size_t len);
cobs_ret_t cobs_decode_tinyframe(void *buf, size_t len);

// cobs_decode
//
Expand Down
4 changes: 2 additions & 2 deletions make-win.bat
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ cl.exe /W4 /WX /MP /EHsc /std:c++20 ^
tests/cobs_encode_max_c.c ^
tests/test_cobs_decode.cc ^
tests/test_cobs_decode_inc.cc ^
tests/test_cobs_decode_inplace.cc ^
tests/test_cobs_decode_tinyframe.cc ^
tests/test_cobs_encode_max.cc ^
tests/test_cobs_encode.cc ^
tests/test_cobs_encode_inc.cc ^
tests/test_cobs_encode_inplace.cc ^
tests/test_cobs_encode_tinyframe.cc ^
tests/test_many_random_payloads.cc ^
tests/test_paper_figures.cc ^
tests/test_wikipedia.cc ^
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
#include <cstring>
#include <numeric>

static constexpr byte_t CSV = COBS_INPLACE_SENTINEL_VALUE;
static constexpr byte_t CSV = COBS_TINYFRAME_SENTINEL_VALUE;

namespace {
cobs_ret_t cobs_decode_vec(byte_vec_t &v) {
return cobs_decode_inplace(v.data(), static_cast<unsigned>(v.size()));
return cobs_decode_tinyframe(v.data(), static_cast<unsigned>(v.size()));
}
} // namespace

TEST_CASE("Inplace decoding validation") {
SUBCASE("Null buffer pointer") {
REQUIRE(cobs_decode_inplace(nullptr, 123) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_decode_tinyframe(nullptr, 123) == COBS_RET_ERR_BAD_ARG);
}

SUBCASE("Invalid buf_len") {
char buf;
REQUIRE(cobs_decode_inplace(&buf, 0) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_decode_inplace(&buf, 1) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_decode_tinyframe(&buf, 0) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_decode_tinyframe(&buf, 1) == COBS_RET_ERR_BAD_ARG);
}

SUBCASE("Invalid payload") {
Expand Down Expand Up @@ -123,16 +123,16 @@ void verify_decode_inplace(unsigned char *inplace, size_t payload_len) {
payload_len);

REQUIRE(external_len == payload_len);
REQUIRE(cobs_decode_inplace(inplace, payload_len + 2) == COBS_RET_SUCCESS);
REQUIRE(cobs_decode_tinyframe(inplace, payload_len + 2) == COBS_RET_SUCCESS);
REQUIRE(byte_vec_t(inplace + 1, inplace + external_len + 1) ==
byte_vec_t(external.data(), external.data() + external_len));
}

void fill_encode_inplace(byte_t *inplace, size_t payload_len, byte_t f) {
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
memset(inplace + 1, f, payload_len);
inplace[payload_len + 1] = COBS_INPLACE_SENTINEL_VALUE;
REQUIRE_MESSAGE(cobs_encode_inplace(inplace, payload_len + 2) == COBS_RET_SUCCESS,
inplace[payload_len + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
REQUIRE_MESSAGE(cobs_encode_tinyframe(inplace, payload_len + 2) == COBS_RET_SUCCESS,
payload_len);
}
} // namespace
Expand Down Expand Up @@ -163,24 +163,24 @@ TEST_CASE("Decode: Inplace == External") {

SUBCASE("Fill with zero/one pattern") {
for (auto i{ 0u }; i < inplace.size() - 2; ++i) {
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
for (auto j{ 1u }; j < i; ++j) {
inplace[j] = j & 1;
}
inplace[i + 1] = COBS_INPLACE_SENTINEL_VALUE;
REQUIRE(cobs_encode_inplace(inplace.data(), i + 2) == COBS_RET_SUCCESS);
inplace[i + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
REQUIRE(cobs_encode_tinyframe(inplace.data(), i + 2) == COBS_RET_SUCCESS);
verify_decode_inplace(inplace.data(), i);
}
}

SUBCASE("Fill with one/zero pattern") {
for (auto i{ 0u }; i < inplace.size() - 2; ++i) {
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
for (auto j = 1u; j < i; ++j) {
inplace[j] = (j & 1) ^ 1;
}
inplace[i + 1] = COBS_INPLACE_SENTINEL_VALUE;
REQUIRE(cobs_encode_inplace(inplace.data(), i + 2) == COBS_RET_SUCCESS);
inplace[i + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
REQUIRE(cobs_encode_tinyframe(inplace.data(), i + 2) == COBS_RET_SUCCESS);
verify_decode_inplace(inplace.data(), i);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
#include <cstring>
#include <numeric>

static constexpr byte_t CSV = COBS_INPLACE_SENTINEL_VALUE;
static constexpr byte_t CSV = COBS_TINYFRAME_SENTINEL_VALUE;

namespace {
cobs_ret_t cobs_encode_vec(byte_vec_t &v) {
return cobs_encode_inplace(v.data(), v.size());
return cobs_encode_tinyframe(v.data(), v.size());
}
} // namespace

TEST_CASE("Inplace encoding validation") {
SUBCASE("Null buffer pointer") {
REQUIRE(cobs_encode_inplace(nullptr, 123) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_encode_tinyframe(nullptr, 123) == COBS_RET_ERR_BAD_ARG);
}

SUBCASE("Invalid buf_len") {
char buf;
REQUIRE(cobs_encode_inplace(&buf, 0) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_encode_inplace(&buf, 1) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_encode_tinyframe(&buf, 0) == COBS_RET_ERR_BAD_ARG);
REQUIRE(cobs_encode_tinyframe(&buf, 1) == COBS_RET_ERR_BAD_ARG);
}

SUBCASE("Invalid sentinel values") {
Expand Down Expand Up @@ -133,14 +133,14 @@ void verify_encode_inplace(byte_t *inplace, size_t payload_len) {
external.size(),
&external_len) == COBS_RET_SUCCESS);

REQUIRE(cobs_encode_inplace(inplace, payload_len + 2) == COBS_RET_SUCCESS);
REQUIRE(cobs_encode_tinyframe(inplace, payload_len + 2) == COBS_RET_SUCCESS);
REQUIRE(byte_vec_t(inplace, inplace + payload_len + 2) == external);
}

void fill_inplace(byte_t *inplace, size_t payload_len, byte_t f) {
memset(inplace + 1, f, payload_len);
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[payload_len + 1] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
inplace[payload_len + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
}
} // namespace

Expand Down Expand Up @@ -170,22 +170,22 @@ TEST_CASE("Encode: Inplace == External") {

SUBCASE("Fill with zero/one pattern") {
for (auto i = 0u; i < sizeof(inplace) - 2; ++i) {
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
for (auto j = 1u; j < i; ++j) {
inplace[j] = j & 1;
}
inplace[i + 1] = COBS_INPLACE_SENTINEL_VALUE;
inplace[i + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
verify_encode_inplace(inplace, i);
}
}

SUBCASE("Fill with one/zero pattern") {
for (auto i = 0u; i < sizeof(inplace) - 2; ++i) {
inplace[0] = COBS_INPLACE_SENTINEL_VALUE;
inplace[0] = COBS_TINYFRAME_SENTINEL_VALUE;
for (auto j = 1u; j < i; ++j) {
inplace[j] = (j & 1) ^ 1;
}
inplace[i + 1] = COBS_INPLACE_SENTINEL_VALUE;
inplace[i + 1] = COBS_TINYFRAME_SENTINEL_VALUE;
verify_encode_inplace(inplace, i);
}
}
Expand Down
8 changes: 4 additions & 4 deletions tests/test_wikipedia.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
namespace {

void round_trip_inplace(byte_vec_t const &decoded, byte_vec_t const &encoded) {
byte_vec_t decoded_inplace{ COBS_INPLACE_SENTINEL_VALUE };
byte_vec_t decoded_inplace{ COBS_TINYFRAME_SENTINEL_VALUE };
decoded_inplace.insert(std::end(decoded_inplace), decoded.begin(), decoded.end());
decoded_inplace.push_back(COBS_INPLACE_SENTINEL_VALUE);
decoded_inplace.push_back(COBS_TINYFRAME_SENTINEL_VALUE);

byte_vec_t x(decoded_inplace);

REQUIRE(cobs_encode_inplace(x.data(), x.size()) == COBS_RET_SUCCESS);
REQUIRE(cobs_encode_tinyframe(x.data(), x.size()) == COBS_RET_SUCCESS);
REQUIRE(x == encoded);
REQUIRE(cobs_decode_inplace(x.data(), x.size()) == COBS_RET_SUCCESS);
REQUIRE(cobs_decode_tinyframe(x.data(), x.size()) == COBS_RET_SUCCESS);
REQUIRE(x == decoded_inplace);
}

Expand Down