{"id":46550830,"url":"https://github.com/charlesnicholson/nanocobs","last_synced_at":"2026-03-07T03:13:54.992Z","repository":{"id":43741524,"uuid":"356054945","full_name":"charlesnicholson/nanocobs","owner":"charlesnicholson","description":"A C99 implementation of the Consistent Overhead Byte Stuffing (\"COBS\") algorithm.","archived":false,"fork":false,"pushed_at":"2026-02-21T19:23:05.000Z","size":410,"stargazers_count":64,"open_issues_count":1,"forks_count":10,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-02-22T00:22:38.113Z","etag":null,"topics":["c","cobs","decoding","encoding","protocols"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/charlesnicholson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-04-08T21:40:27.000Z","updated_at":"2026-02-21T19:33:18.000Z","dependencies_parsed_at":"2024-01-08T19:32:16.441Z","dependency_job_id":"5c95205e-6dcb-4688-983c-aa848145381e","html_url":"https://github.com/charlesnicholson/nanocobs","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/charlesnicholson/nanocobs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charlesnicholson%2Fnanocobs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charlesnicholson%2Fnanocobs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charlesnicholson%2Fnanocobs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charlesnicholson%2Fnanocobs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/charlesnicholson","download_url":"https://codeload.github.com/charlesnicholson/nanocobs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/charlesnicholson%2Fnanocobs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30206356,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T19:07:06.838Z","status":"online","status_checked_at":"2026-03-07T02:00:06.765Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","cobs","decoding","encoding","protocols"],"created_at":"2026-03-07T03:13:54.430Z","updated_at":"2026-03-07T03:13:54.956Z","avatar_url":"https://github.com/charlesnicholson.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nanocobs\n\n`nanocobs` is a C99 implementation of the [Consistent Overhead Byte Stuffing](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) (\"COBS\") algorithm, defined in the [paper](http://www.stuartcheshire.org/papers/COBSforToN.pdf) by Stuart Cheshire and Mary Baker.\n\nUsers can encode and decode data in-place or into separate target buffers. Encoding and decoding can both be incremental, streaming data through small caller-provided buffers. The `nanocobs` runtime requires no extra memory overhead. No standard library headers are included, and no standard library functions are called.\n\n## Rationale\n\nSome communication buses (e.g. two-wire UART) are inherently unreliable and have no built-in flow control, integrity guarantees, etc. Multi-hop ecosystem protocols (e.g. Device (BLE) Phone (HTTPS) Server) can also be unreliable, despite being comprised entirely of reliable protocols! Adding integrity checks like CRC can help, but only if the data is [framed](https://en.wikipedia.org/wiki/Frame_(networking)); without framing, the receiver and transmitter are unable to agree exactly what data needs to be retransmitted when loss is detected. Loss does not always have to be due to interference or corruption during transmission, either. Application-level backpressure can exhaust receiver-side storage and result in dropped frames.\n\nTraditional solutions (like [CAN](https://en.wikipedia.org/wiki/CAN_bus)) rely on [bit stuffing](https://en.wikipedia.org/wiki/Bit_stuffing) to define frame boundaries. This works fine, but can be subtle and complex to implement in software without dedicated hardware.\n\n`nanocobs` is not a general-purpose reliability solution, but it can be used as the lowest-level framing algorithm required by a reliable transport.\n\nYou probably only need `nanocobs` for things like inter-chip communications protocols on embedded devices. If you already have a reliable transport from somewhere else, you might enjoy using that instead of building your own :)\n\n### Why another COBS?\n\nThere are a few out there, but I haven't seen any that optionally encode in-place. This can be handy if you're memory-constrained and would enjoy CPU + RAM optimizations that come from using small frames. Also, the cost of in-place decoding is only as expensive as the number of zeroes in your payload; exploiting that if you're designing your own protocols can make decoding very fast.\n\nNone of the other COBS implementations I saw supported incremental encoding and decoding. It's often the case in communication stacks that a layer above the link provides a tightly-sized payload buffer, and the link has to encode both a header _and_ this payload into a single frame. That requires an extra buffer for assembling which then immediately gets encoded into yet another buffer. With incremental encoding, data can be streamed through small buffers without ever needing to allocate the full encoded frame.\n\nFinally, I didn't see as many unit tests as I'd have liked in the other libraries, especially around invalid payload handling. Framing protocols make for lovely attack surfaces, and malicious COBS frames can easily instruct decoders to jump outside of the frame itself.\n\n## Metrics\n\nIt's pretty small, and you probably need either `cobs_[en|de]code_tinyframe` _or_ `cobs_[en|de]code[_inc*]`, but not all of them.\n```\n❯ arm-none-eabi-gcc -mthumb -mcpu=cortex-m4 -Os -c cobs.c\n❯ arm-none-eabi-nm --print-size --size-sort cobs.o\n\n000002c4 0000000e T cobs_decode_inc_begin  (14 bytes)\n00000128 0000001c T cobs_encode_inc_begin  (28 bytes)\n00000000 00000022 t flush_block            (34 bytes)\n00000396 00000044 T cobs_decode            (68 bytes)\n0000006a 00000048 T cobs_decode_tinyframe  (72 bytes)\n00000022 00000048 T cobs_encode_tinyframe  (72 bytes)\n000000b2 00000076 T cobs_encode            (118 bytes)\n00000212 000000b2 T cobs_encode_inc_end    (178 bytes)\n000002d2 000000c4 T cobs_decode_inc        (196 bytes)\n00000144 000000ce T cobs_encode_inc        (206 bytes)\nTotal 3da (986 bytes)\n```\n\n## Usage\n\nCompile `cobs.c` and link it into your app. `#include \"path/to/cobs.h\"` in your source code. Call functions.\n\n### Encoding\n\nFill a buffer with the data you'd like to encode. Prepare a larger buffer to hold the encoded data. Then, call `cobs_encode` to encode the data into the destination buffer.\n\n```c\nunsigned char decoded[64];\nunsigned const len = fill_with_decoded_data(decoded);\n\nunsigned char encoded[128];\nunsigned encoded_len;\ncobs_ret_t const result = cobs_encode(decoded, len, encoded, sizeof(encoded), \u0026encoded_len);\n\nif (result == COBS_RET_SUCCESS) {\n  // encoding succeeded, 'encoded' and 'encoded_len' hold details.\n} else {\n  // encoding failed, look to 'result' for details.\n}\n```\n\n### Decoding\n\nDecoding works similarly; receive an encoded buffer from somewhere, prepare a buffer to hold the decoded data, and call `cobs_decode`.\n\n```c\nunsigned char encoded[128];\nunsigned encoded_len;\nget_encoded_data_from_somewhere(encoded, \u0026encoded_len);\n\nunsigned char decoded[128];\nunsigned decoded_len;\ncobs_ret_t const result = cobs_decode(encoded, encoded_len, decoded, sizeof(decoded), \u0026decoded_len);\n\nif (result == COBS_RET_SUCCESS) {\n  // decoding succeeded, 'decoded' and 'decoded_len' hold details.\n} else {\n  // decoding failed, look to 'result' for details.\n}\n```\n\n### Incremental Encoding\n\nThe incremental encoding API lets you stream COBS-encoded data through small buffers. Each call to `cobs_encode_inc` takes per-call source and destination buffers, reporting how many bytes were consumed and written. A 255-byte work buffer (provided by the caller) holds the current in-progress block internally.\n\nThis is ideal for memory-constrained embedded systems that can't allocate `COBS_ENCODE_MAX(n)` bytes up front.\n\n```c\nunsigned char work_buf[255];       // user-provided, must remain valid until end()\ncobs_enc_ctx_t ctx;\ncobs_ret_t r = cobs_encode_inc_begin(\u0026ctx, work_buf, sizeof(work_buf));\n\nunsigned char header[8];\nunsigned header_len = get_header(header);\n\nunsigned char out[64];             // small output buffer, reused each call\nsize_t src_consumed, dst_written;\n\ncobs_encode_inc_args_t args;\nargs.dec_src = header;\nargs.enc_dst = out;\nargs.dec_src_max = header_len;\nargs.enc_dst_max = sizeof(out);\nr = cobs_encode_inc(\u0026ctx, \u0026args, \u0026src_consumed, \u0026dst_written);\n// send out[0..dst_written) downstream\n\nunsigned char const *payload;\nunsigned payload_len = get_payload(\u0026payload);\n\n// Feed payload in chunks; small output buffers are fine\nsize_t pos = 0;\nwhile (pos \u003c payload_len) {\n  args.dec_src = payload + pos;\n  args.enc_dst = out;\n  args.dec_src_max = payload_len - pos;\n  args.enc_dst_max = sizeof(out);\n  r = cobs_encode_inc(\u0026ctx, \u0026args, \u0026src_consumed, \u0026dst_written);\n  // send out[0..dst_written) downstream\n  pos += src_consumed;\n}\n\n// Flush the final block + delimiter (may need multiple calls with tiny buffers)\nbool finished = false;\nwhile (!finished) {\n  r = cobs_encode_inc_end(\u0026ctx, out, sizeof(out), \u0026dst_written, \u0026finished);\n  // send out[0..dst_written) downstream\n}\n```\n\nIf your output buffer is large enough (e.g. `COBS_ENCODE_MAX(n)` bytes), each call consumes all source bytes and `end()` finishes in one call. With smaller output buffers, the encoder pauses mid-flush and resumes on the next call.\n\n### Incremental Decoding\n\nThe incremental decoding API mirrors the encoding API. Each call to `cobs_decode_inc` takes per-call source and destination buffers, reporting how many encoded bytes were consumed, how many decoded bytes were written, and whether the frame delimiter has been reached.\n\n```c\ncobs_decode_inc_ctx_t ctx;\ncobs_ret_t r = cobs_decode_inc_begin(\u0026ctx);\n\nunsigned char enc[64];             // small input buffer, filled from uart/etc\nunsigned char dec[64];             // small output buffer\nsize_t src_consumed, dst_written;\nbool complete = false;\n\nwhile (!complete) {\n  unsigned enc_len = read_encoded_chunk(enc, sizeof(enc));\n\n  cobs_decode_inc_args_t args;\n  args.enc_src = enc;\n  args.dec_dst = dec;\n  args.enc_src_max = enc_len;\n  args.dec_dst_max = sizeof(dec);\n  r = cobs_decode_inc(\u0026ctx, \u0026args, \u0026src_consumed, \u0026dst_written, \u0026complete);\n  // process dec[0..dst_written) upstream\n}\n```\n\n### Tinyframe Encoding\n\nIf you can guarantee that your payloads are shorter than 254 bytes, you can use the tinyframe API to encode and decode 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.\n\n(Note that `64` is an arbitrary size in this example, you can use any size you want up to `COBS_TINYFRAME_SAFE_BUFFER_SIZE`)\n\n```c\nunsigned char buf[64];\nbuf[0] = COBS_TINYFRAME_SENTINEL_VALUE; // You have to do this.\nbuf[63] = COBS_TINYFRAME_SENTINEL_VALUE; // You have to do this.\n\n// Now, fill in buf[1] up to and including buf[62] (so 64 - 2 = 62 bytes of payload data)\n\ncobs_ret_t const result = cobs_encode_tinyframe(buf, 64);\n\nif (result == COBS_RET_SUCCESS) {\n  // encoding succeeded, 'buf' now holds the encoded data.\n} else {\n  // encoding failed, look to 'result' for details.\n}\n```\n\n### Tinyframe Decoding\n\n`cobs_decode_tinyframe` offers byte-layout-parity with `cobs_encode_tinyframe`. This lets you decode a payload, change some bytes, and re-encode it all in the same buffer.\n\nAccumulate data from your source until you encounter a COBS frame delimiter byte of `0x00`. Once you've got that, call `cobs_decode_tinyframe` 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_TINYFRAME_SENTINEL_VALUE` bytes that, were you _encoding_ in-place, you would have had to place there anyway.\n\n```c\nunsigned char buf[64];\n\n// You fill 'buf' with an encoded cobs frame (from uart, etc) that ends with 0x00.\nunsigned const length = you_fill_buf_with_data(buf);\n\ncobs_ret_t const result = cobs_decode_tinyframe(buf, length);\nif (result == COBS_RET_SUCCESS) {\n  // decoding succeeded, 'buf' bytes 0 and length-1 are COBS_TINYFRAME_SENTINEL_VALUE.\n  // your data is in 'buf[1 ... length-2]'\n} else {\n  // decoding failed, look to 'result' for details.\n}\n```\n\n## Developing\n\n`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).\n\nThe presubmit workflow compiles `nanocobs` on macOS, Linux (gcc) 32/64, Windows (msvc) 32/64. It also builds weekly against a fresh docker image so I know when newer stricter compilers break it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharlesnicholson%2Fnanocobs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcharlesnicholson%2Fnanocobs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcharlesnicholson%2Fnanocobs/lists"}