feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"files":{".cargo_vcs_info.json":"d7f67f249a331bde67cf8162e29befd8924a945e8f84cf61fa5e0219e87f7a63",".github/workflows/ci.yml":"b256b0b4073ff834dfaf2421e2728f33dc042e0b880e51cbef5518e37f517e28","CHANGELOG.md":"cbb63c04a29efeaa5b50a4a64312a85d76f0cc1733431161ebc4ab7a731862e8","Cargo.toml":"14a58435fc68ea809caa3803b81b052bd657c90fea71cdcb434459aac39ffc5e","Cargo.toml.orig":"ef01823a9f787c8ee0aa0b3bb4b9271c79770f98828ad3172d687bc6eb31dd0d","LICENSE-APACHE":"c6596eb7be8581c18be736c846fb9173b69eccf6ef94c5135893ec56bd92ba08","LICENSE-MIT":"e0cfa1006a64520633de6bfbf563f5b1bea04ef0c5b73f049681931fa297dda3","README.md":"1d7f49fdb966089ecc179012845b4bce07d0faa6f2ff1e624b1e9b76f41ec6da","docs.sh":"517bd875fc2367a1ca9b34aaefc9ddcd4a34f9b76924c591db6d55d980cd4545","src/dec.rs":"942f4d43c64c87721a31ad25473f1a036d9b60c99a2c73d315f4511d4982ac35","src/enc.rs":"9424997cec456ee63364d720fc25302361fc612dca3c6f1a4101f400aef57062","src/lib.rs":"0ea22d527da4aa9c4688b98772b68753337efea553198ec6b032a2cae819eb2d","tests/tests.rs":"de4c76a70f5e10425d3361114ae30b41e8d4a82755fe3167aaa3567040b865a6"},"package":"0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "b86d5c1ace3d8c311d1db143f05f8083743700d1"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check build
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo check --release
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- run: cargo nextest run --all-features
|
||||
|
||||
cross-check:
|
||||
name: Check Cross-Compilation
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- armv7-unknown-linux-gnueabihf
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf"
|
||||
- run: cargo check --release --target=${{matrix.target}} --no-default-features
|
||||
|
||||
fmt:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
docs:
|
||||
name: Check Documentation Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- run: RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo clippy -- -D warnings
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
Change Log
|
||||
=======
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# [unreleased]
|
||||
|
||||
# [v0.3.0] 2025-01-31
|
||||
|
||||
## Added
|
||||
|
||||
- Added `max_encoding_overhead` const function.
|
||||
[#20](https://github.com/jamesmunns/cobs.rs/pull/20)
|
||||
- Unittests for `decode_in_place` and `decode_in_place_report`.
|
||||
[#36](https://github.com/jamesmunns/cobs.rs/pull/36)
|
||||
- New `alloc` feature which enables support for APIs returning `Vec`s.
|
||||
[#24](https://github.com/jamesmunns/cobs.rs/pull/24)
|
||||
- New `std` feature, deprecate the `use_std` feature but keep it for backwards compatiblity.
|
||||
[#38](https://github.com/jamesmunns/cobs.rs/pull/38)
|
||||
- Documentation of the new `std` and `alloc` features in the README.
|
||||
[#38](https://github.com/jamesmunns/cobs.rs/pull/38)
|
||||
- Optional `defmt` support feature-gated by the optional `defmt` feature.
|
||||
[#40](https://github.com/jamesmunns/cobs.rs/pull/40)
|
||||
- Add optional `serde` support that adds `serde` derives for some errors and data structures.
|
||||
[#41](https://github.com/jamesmunns/cobs.rs/pull/41)
|
||||
|
||||
## Changed
|
||||
|
||||
- Bumped rust edition to 2021
|
||||
- Improved error handling by replacing unit errors with a new `DecodeError` enumeration for
|
||||
the decoding module and a `DestBufTooSmallError` for the encoding module.
|
||||
[#30](https://github.com/jamesmunns/cobs.rs/pull/30)
|
||||
- `max_encoding_length` is now a const function. [#20](https://github.com/jamesmunns/cobs.rs/pull/20)
|
||||
- `decode_in_place` and `decode_in_place_report` now check the destination index and return
|
||||
an appropriate `DecodeError` instead of panicking if an out-of-bounds access happens.
|
||||
[#36](https://github.com/jamesmunns/cobs.rs/pull/36)
|
||||
- Consistent behaviour of decode functions for empty input: All funtions now return
|
||||
`DecodeError::FrameEmpty`.
|
||||
[#39](https://github.com/jamesmunns/cobs.rs/pull/39)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed wrong encoded length when source length was divisible by 254.
|
||||
[#19](https://github.com/jamesmunns/cobs.rs/issues/19)
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
authors = [
|
||||
"Allen Welkie <>",
|
||||
"James Munns <james@onevariable.com>",
|
||||
]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = '''
|
||||
This is an implementation of the Consistent Overhead Byte Stuffing (COBS) algorithm.
|
||||
COBS is an algorithm for transforming a message into an encoding where a specific value
|
||||
(the "sentinel" value) is not used. This value can then be used to mark frame boundaries
|
||||
in a serial communication channel. '''
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"consistent",
|
||||
"overhead",
|
||||
"byte",
|
||||
"stuffing",
|
||||
"encoding",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/jamesmunns/cobs.rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[lib]
|
||||
name = "cobs"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[test]]
|
||||
name = "tests"
|
||||
path = "tests/tests.rs"
|
||||
|
||||
[dependencies.defmt]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.quickcheck]
|
||||
version = "1"
|
||||
|
||||
[features]
|
||||
alloc = []
|
||||
default = ["std"]
|
||||
std = [
|
||||
"alloc",
|
||||
"thiserror/std",
|
||||
]
|
||||
use_std = ["std"]
|
||||
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015 The cobs.rs Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
# `cobs`
|
||||
|
||||
This is an implementation of the Consistent Overhead Byte Stuffing (COBS) algorithm in Rust.
|
||||
|
||||
COBS is an algorithm for transforming a message into an encoding where a specific value (the "sentinel" value) is not used. This value can then be used to mark frame boundaries in a serial communication channel.
|
||||
|
||||
See [the wikipedia article](https://www.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) for details.
|
||||
|
||||
## Features
|
||||
|
||||
`cobs` supports various runtime environments and is also suitable for `no_std` environments.
|
||||
|
||||
### Default features
|
||||
|
||||
- [`std`](https://doc.rust-lang.org/std/): Enables functionality relying on the standard library
|
||||
and also activates the `alloc` feature. Currently only adds `std::error::Error` support for the
|
||||
library error types.
|
||||
- [`alloc`](https://doc.rust-lang.org/alloc/): Enables features which operate on containers
|
||||
like [`alloc::vec::Vec`](https://doc.rust-lang.org/beta/alloc/vec/struct.Vec.html).
|
||||
Enabled by the `std` feature.
|
||||
|
||||
### Optional features
|
||||
|
||||
- [`defmt`](https://docs.rs/defmt/latest/defmt/): Adds `defmt::Format` derives on some data
|
||||
structures and error types.
|
||||
- [`serde`](https://serde.rs/): Adds `serde` derives on some data structures and error types.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
|
||||
cargo +nightly doc --all-features --open
|
||||
Vendored
+413
@@ -0,0 +1,413 @@
|
||||
/// The [`CobsDecoder`] type is used to decode a stream of bytes to a
|
||||
/// given mutable output slice. This is often useful when heap data
|
||||
/// structures are not available, or when not all message bytes are
|
||||
/// received at a single point in time.
|
||||
#[derive(Debug)]
|
||||
pub struct CobsDecoder<'a> {
|
||||
/// Destination slice for decoded message
|
||||
dest: &'a mut [u8],
|
||||
|
||||
/// Index of next byte to write in `dest`
|
||||
dest_idx: usize,
|
||||
|
||||
/// Decoder state as an enum
|
||||
state: DecoderState,
|
||||
}
|
||||
|
||||
/// The [`DecoderState`] is used to track the current state of a
|
||||
/// streaming decoder. This struct does not contain the output buffer
|
||||
/// (or a reference to one), and can be used when streaming the decoded
|
||||
/// output to a custom data type.
|
||||
#[derive(Debug)]
|
||||
pub enum DecoderState {
|
||||
/// State machine has not received any non-zero bytes
|
||||
Idle,
|
||||
|
||||
/// 1-254 bytes, can be header or 00
|
||||
Grab(u8),
|
||||
|
||||
/// 255 bytes, will be a header next
|
||||
GrabChain(u8),
|
||||
}
|
||||
|
||||
fn add(to: &mut [u8], idx: usize, data: u8) -> Result<(), DecodeError> {
|
||||
*to.get_mut(idx).ok_or(DecodeError::TargetBufTooSmall)? = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`DecodeResult`] represents the possible non-error outcomes of
|
||||
/// pushing an encoded data byte into the [`DecoderState`] state machine
|
||||
pub enum DecodeResult {
|
||||
/// The given input byte did not prompt an output byte, either because the
|
||||
/// state machine is still idle, or we have just processed a header byte.
|
||||
/// More data is needed to complete the message.
|
||||
NoData,
|
||||
|
||||
/// We have received a complete and well-encoded COBS message. The
|
||||
/// contents of the associated output buffer may now be used
|
||||
DataComplete,
|
||||
|
||||
/// The following byte should be appended to the current end of the decoded
|
||||
/// output buffer.
|
||||
/// More data is needed to complete the message.
|
||||
DataContinue(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum DecodeError {
|
||||
#[error("empty input frame")]
|
||||
EmptyFrame,
|
||||
#[error("frame with invalid format, written {decoded_bytes:?} to decoded buffer")]
|
||||
InvalidFrame {
|
||||
/// Number of bytes written to the decoded buffer.
|
||||
decoded_bytes: usize,
|
||||
},
|
||||
#[error("target buffer too small")]
|
||||
TargetBufTooSmall,
|
||||
}
|
||||
|
||||
impl DecoderState {
|
||||
/// Push a single encoded byte into the state machine. If the input was
|
||||
/// unexpected, such as an early end of a framed message segment, an Error will
|
||||
/// be returned, and the current associated output buffer contents should be discarded.
|
||||
///
|
||||
/// If a complete message is indicated, the decoding state machine will automatically
|
||||
/// reset itself to the Idle state, and may be used to begin decoding another message.
|
||||
///
|
||||
/// NOTE: Sentinel value must be included in the input to this function for the
|
||||
/// decoding to complete
|
||||
pub fn feed(&mut self, data: u8) -> Result<DecodeResult, DecodeError> {
|
||||
use DecodeResult::*;
|
||||
use DecoderState::*;
|
||||
let (ret, state) = match (&self, data) {
|
||||
// Currently Idle, received a terminator, ignore, stay idle
|
||||
(Idle, 0x00) => (Ok(NoData), Idle),
|
||||
|
||||
// Currently Idle, received a byte indicating the
|
||||
// next 255 bytes have no zeroes, so we will have 254 unmodified
|
||||
// data bytes, then an overhead byte
|
||||
(Idle, 0xFF) => (Ok(NoData), GrabChain(0xFE)),
|
||||
|
||||
// Currently Idle, received a byte indicating there will be a
|
||||
// zero that must be modified in the next 1..=254 bytes
|
||||
(Idle, n) => (Ok(NoData), Grab(n - 1)),
|
||||
|
||||
// We have reached the end of a data run indicated by an overhead
|
||||
// byte, AND we have recieved the message terminator. This was a
|
||||
// well framed message!
|
||||
(Grab(0), 0x00) => (Ok(DataComplete), Idle),
|
||||
|
||||
// We have reached the end of a data run indicated by an overhead
|
||||
// byte, and the next segment of 254 bytes will have no modified
|
||||
// sentinel bytes
|
||||
(Grab(0), 0xFF) => (Ok(DataContinue(0)), GrabChain(0xFE)),
|
||||
|
||||
// We have reached the end of a data run indicated by an overhead
|
||||
// byte, and we will treat this byte as a modified sentinel byte.
|
||||
// place the sentinel byte in the output, and begin processing the
|
||||
// next non-sentinel sequence
|
||||
(Grab(0), n) => (Ok(DataContinue(0)), Grab(n - 1)),
|
||||
|
||||
// We were not expecting the sequence to terminate, but here we are.
|
||||
// Report an error due to early terminated message
|
||||
(Grab(_), 0) => (Err(DecodeError::InvalidFrame { decoded_bytes: 0 }), Idle),
|
||||
|
||||
// We have not yet reached the end of a data run, decrement the run
|
||||
// counter, and place the byte into the decoded output
|
||||
(Grab(i), n) => (Ok(DataContinue(n)), Grab(*i - 1)),
|
||||
|
||||
// We have reached the end of a data run indicated by an overhead
|
||||
// byte, AND we have recieved the message terminator. This was a
|
||||
// well framed message!
|
||||
(GrabChain(0), 0x00) => (Ok(DataComplete), Idle),
|
||||
|
||||
// We have reached the end of a data run, and we will begin another
|
||||
// data run with an overhead byte expected at the end
|
||||
(GrabChain(0), 0xFF) => (Ok(NoData), GrabChain(0xFE)),
|
||||
|
||||
// We have reached the end of a data run, and we will expect `n` data
|
||||
// bytes unmodified, followed by a sentinel byte that must be modified
|
||||
(GrabChain(0), n) => (Ok(NoData), Grab(n - 1)),
|
||||
|
||||
// We were not expecting the sequence to terminate, but here we are.
|
||||
// Report an error due to early terminated message
|
||||
(GrabChain(_), 0) => (Err(DecodeError::InvalidFrame { decoded_bytes: 0 }), Idle),
|
||||
|
||||
// We have not yet reached the end of a data run, decrement the run
|
||||
// counter, and place the byte into the decoded output
|
||||
(GrabChain(i), n) => (Ok(DataContinue(n)), GrabChain(*i - 1)),
|
||||
};
|
||||
|
||||
*self = state;
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CobsDecoder<'a> {
|
||||
/// Create a new streaming Cobs Decoder. Provide the output buffer
|
||||
/// for the decoded message to be placed in
|
||||
pub fn new(dest: &'a mut [u8]) -> CobsDecoder<'a> {
|
||||
CobsDecoder {
|
||||
dest,
|
||||
dest_idx: 0,
|
||||
state: DecoderState::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a single byte into the streaming CobsDecoder. Return values mean:
|
||||
///
|
||||
/// * Ok(None) - State machine okay, more data needed
|
||||
/// * Ok(Some(N)) - A message of N bytes was successfully decoded
|
||||
/// * Err([DecodeError]) - Message decoding failed
|
||||
///
|
||||
/// NOTE: Sentinel value must be included in the input to this function for the
|
||||
/// decoding to complete
|
||||
pub fn feed(&mut self, data: u8) -> Result<Option<usize>, DecodeError> {
|
||||
match self.state.feed(data) {
|
||||
Err(_) => Err(DecodeError::InvalidFrame {
|
||||
decoded_bytes: self.dest_idx,
|
||||
}),
|
||||
Ok(DecodeResult::NoData) => Ok(None),
|
||||
Ok(DecodeResult::DataContinue(n)) => {
|
||||
add(self.dest, self.dest_idx, n)?;
|
||||
self.dest_idx += 1;
|
||||
Ok(None)
|
||||
}
|
||||
Ok(DecodeResult::DataComplete) => Ok(Some(self.dest_idx)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a slice of bytes into the streaming CobsDecoder. Return values mean:
|
||||
///
|
||||
/// * Ok(None) - State machine okay, more data needed
|
||||
/// * Ok(Some((N, M))) - A message of N bytes was successfully decoded,
|
||||
/// using M bytes from `data` (and earlier data)
|
||||
/// * Err([DecodeError]) - Message decoding failed
|
||||
///
|
||||
/// NOTE: Sentinel value must be included in the input to this function for the
|
||||
/// decoding to complete
|
||||
pub fn push(&mut self, data: &[u8]) -> Result<Option<(usize, usize)>, DecodeError> {
|
||||
for (consumed_idx, d) in data.iter().enumerate() {
|
||||
let opt_decoded_bytes = self.feed(*d)?;
|
||||
if let Some(decoded_bytes_ct) = opt_decoded_bytes {
|
||||
// convert from index to number of bytes consumed
|
||||
return Ok(Some((decoded_bytes_ct, consumed_idx + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the `source` buffer into the `dest` buffer.
|
||||
///
|
||||
/// This function uses the typical sentinel value of 0.
|
||||
///
|
||||
/// # Failures
|
||||
///
|
||||
/// This will return `Err(())` if there was a decoding error. Otherwise,
|
||||
/// it will return `Ok(n)` where `n` is the length of the decoded message.
|
||||
pub fn decode(source: &[u8], dest: &mut [u8]) -> Result<usize, DecodeError> {
|
||||
if source.is_empty() {
|
||||
return Err(DecodeError::EmptyFrame);
|
||||
}
|
||||
|
||||
let mut dec = CobsDecoder::new(dest);
|
||||
|
||||
// Did we decode a message, using some or all of the buffer?
|
||||
if let Some((d_used, _s_used)) = dec.push(source)? {
|
||||
return Ok(d_used);
|
||||
}
|
||||
|
||||
// If we consumed the entire buffer, but did NOT get a message,
|
||||
// AND the message did not end with a zero, try providing one to
|
||||
// complete the decoding.
|
||||
if source.last() != Some(&0) {
|
||||
// Explicitly push sentinel of zero
|
||||
if let Some((d_used, _s_used)) = dec.push(&[0])? {
|
||||
return Ok(d_used);
|
||||
}
|
||||
}
|
||||
|
||||
// Nope, no early message, no missing terminator, just failed to decode
|
||||
Err(DecodeError::InvalidFrame {
|
||||
decoded_bytes: dec.dest_idx,
|
||||
})
|
||||
}
|
||||
|
||||
/// A report of the source and destination bytes used during in-place decoding
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct DecodeReport {
|
||||
// The number of source bytes used, NOT INCLUDING the sentinel byte,
|
||||
// if there was one.
|
||||
pub src_used: usize,
|
||||
|
||||
// The number of bytes of the source buffer that now include the
|
||||
// decoded result
|
||||
pub dst_used: usize,
|
||||
}
|
||||
|
||||
/// Decodes a message in-place.
|
||||
///
|
||||
/// This is the same function as [decode_in_place], but provides a report
|
||||
/// of both the number of source bytes consumed as well as the size of the
|
||||
/// destination used.
|
||||
pub fn decode_in_place_report(buf: &mut [u8]) -> Result<DecodeReport, DecodeError> {
|
||||
let mut source_index = 0;
|
||||
let mut dest_index = 0;
|
||||
|
||||
if buf.is_empty() {
|
||||
return Err(DecodeError::EmptyFrame);
|
||||
}
|
||||
|
||||
// Stop at the first terminator, if any
|
||||
let src_end = if let Some(end) = buf.iter().position(|b| *b == 0) {
|
||||
end
|
||||
} else {
|
||||
buf.len()
|
||||
};
|
||||
|
||||
while source_index < src_end {
|
||||
let code = buf[source_index];
|
||||
|
||||
if source_index + code as usize > src_end && code != 1 {
|
||||
return Err(DecodeError::InvalidFrame {
|
||||
decoded_bytes: dest_index,
|
||||
});
|
||||
}
|
||||
|
||||
source_index += 1;
|
||||
|
||||
for _ in 1..code {
|
||||
*buf.get_mut(dest_index)
|
||||
.ok_or(DecodeError::TargetBufTooSmall)? = buf[source_index];
|
||||
source_index += 1;
|
||||
dest_index += 1;
|
||||
}
|
||||
|
||||
if 0xFF != code && source_index < src_end {
|
||||
*buf.get_mut(dest_index)
|
||||
.ok_or(DecodeError::TargetBufTooSmall)? = 0;
|
||||
dest_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DecodeReport {
|
||||
dst_used: dest_index,
|
||||
src_used: source_index,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes a message in-place.
|
||||
///
|
||||
/// This is the same function as [decode], but replaces the encoded message
|
||||
/// with the decoded message instead of writing to another buffer.
|
||||
///
|
||||
/// The returned `usize` is the number of bytes used for the DECODED value,
|
||||
/// NOT the number of source bytes consumed during decoding.
|
||||
pub fn decode_in_place(buff: &mut [u8]) -> Result<usize, DecodeError> {
|
||||
decode_in_place_report(buff).map(|r| r.dst_used)
|
||||
}
|
||||
|
||||
/// Decodes the `source` buffer into the `dest` buffer using an arbitrary sentinel value.
|
||||
///
|
||||
/// This is done by XOR-ing each byte of the source message with the chosen sentinel value,
|
||||
/// which transforms the message into the same message encoded with a sentinel value of 0.
|
||||
/// Then the regular decoding transformation is performed.
|
||||
///
|
||||
/// The returned `usize` is the number of bytes used for the DECODED value,
|
||||
/// NOT the number of source bytes consumed during decoding.
|
||||
pub fn decode_with_sentinel(
|
||||
source: &[u8],
|
||||
dest: &mut [u8],
|
||||
sentinel: u8,
|
||||
) -> Result<usize, DecodeError> {
|
||||
for (x, y) in source.iter().zip(dest.iter_mut()) {
|
||||
*y = *x ^ sentinel;
|
||||
}
|
||||
decode_in_place(dest)
|
||||
}
|
||||
|
||||
/// Decodes a message in-place using an arbitrary sentinel value.
|
||||
///
|
||||
/// The returned `usize` is the number of bytes used for the DECODED value,
|
||||
/// NOT the number of source bytes consumed during decoding.
|
||||
pub fn decode_in_place_with_sentinel(buff: &mut [u8], sentinel: u8) -> Result<usize, DecodeError> {
|
||||
for x in buff.iter_mut() {
|
||||
*x ^= sentinel;
|
||||
}
|
||||
decode_in_place(buff)
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
/// Decodes the `source` buffer into a vector.
|
||||
pub fn decode_vec(source: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
|
||||
let mut decoded = alloc::vec![0; source.len()];
|
||||
let n = decode(source, &mut decoded[..])?;
|
||||
decoded.truncate(n);
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
/// Decodes the `source` buffer into a vector with an arbitrary sentinel value.
|
||||
pub fn decode_vec_with_sentinel(
|
||||
source: &[u8],
|
||||
sentinel: u8,
|
||||
) -> Result<alloc::vec::Vec<u8>, DecodeError> {
|
||||
let mut decoded = alloc::vec![0; source.len()];
|
||||
let n = decode_with_sentinel(source, &mut decoded[..], sentinel)?;
|
||||
decoded.truncate(n);
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_malforemd() {
|
||||
let malformed_buf: [u8; 32] = [
|
||||
68, 69, 65, 68, 66, 69, 69, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
let mut dest_buf: [u8; 32] = [0; 32];
|
||||
if let Err(DecodeError::InvalidFrame { decoded_bytes }) =
|
||||
decode(&malformed_buf, &mut dest_buf)
|
||||
{
|
||||
assert_eq!(decoded_bytes, 7);
|
||||
} else {
|
||||
panic!("decoding worked when it should not have");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_empty() {
|
||||
#[cfg(feature = "alloc")]
|
||||
matches!(decode_vec(&[]).unwrap_err(), DecodeError::EmptyFrame);
|
||||
matches!(
|
||||
decode_in_place(&mut []).unwrap_err(),
|
||||
DecodeError::EmptyFrame
|
||||
);
|
||||
matches!(
|
||||
decode(&[], &mut [0; 256]).unwrap_err(),
|
||||
DecodeError::EmptyFrame
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn decode_target_buf_too_small() {
|
||||
let encoded = &[3, 10, 11, 2, 12];
|
||||
let expected_decoded_len = 4;
|
||||
for i in 0..expected_decoded_len - 1 {
|
||||
let mut dest = alloc::vec![0; i];
|
||||
let result = decode(encoded, &mut dest);
|
||||
assert_eq!(result, Err(DecodeError::TargetBufTooSmall));
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+286
@@ -0,0 +1,286 @@
|
||||
/// The [`CobsEncoder`] type is used to encode a stream of bytes to a
|
||||
/// given mutable output slice. This is often useful when heap data
|
||||
/// structures are not available, or when not all message bytes are
|
||||
/// received at a single point in time.
|
||||
#[derive(Debug)]
|
||||
pub struct CobsEncoder<'a> {
|
||||
dest: &'a mut [u8],
|
||||
dest_idx: usize,
|
||||
state: EncoderState,
|
||||
might_be_done: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
#[error("out of bounds error during encoding")]
|
||||
pub struct DestBufTooSmallError;
|
||||
|
||||
/// The [`EncoderState`] is used to track the current state of a
|
||||
/// streaming encoder. This struct does not contain the output buffer
|
||||
/// (or a reference to one), and can be used when streaming the encoded
|
||||
/// output to a custom data type
|
||||
///
|
||||
/// **IMPORTANT NOTE**: When implementing a custom streaming encoder,
|
||||
/// the [`EncoderState`] state machine assumes that the output buffer
|
||||
/// **ALREADY** contains a single placeholder byte, and no other bytes.
|
||||
/// This placeholder byte will be later modified with the first distance
|
||||
/// to the next header/zero byte.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EncoderState {
|
||||
code_idx: usize,
|
||||
num_bt_sent: u8,
|
||||
offset_idx: u8,
|
||||
}
|
||||
|
||||
/// [`PushResult`] is used to represent the changes to an (encoded)
|
||||
/// output data buffer when an unencoded byte is pushed into [`EncoderState`].
|
||||
pub enum PushResult {
|
||||
/// The returned byte should be placed at the current end of the data buffer
|
||||
AddSingle(u8),
|
||||
|
||||
/// The byte at the given index should be replaced with the given byte.
|
||||
/// Additionally, a placeholder byte should be inserted at the current
|
||||
/// end of the output buffer to be later modified
|
||||
ModifyFromStartAndSkip((usize, u8)),
|
||||
|
||||
/// The byte at the given index should be replaced with the given byte.
|
||||
/// Then, the last u8 in this tuple should be inserted at the end of the
|
||||
/// current output buffer. Finally, a placeholder byte should be inserted at
|
||||
/// the current end of the output buffer to be later modified if the encoding process is
|
||||
/// not done yet.
|
||||
ModifyFromStartAndPushAndSkip((usize, u8, u8)),
|
||||
}
|
||||
|
||||
impl Default for EncoderState {
|
||||
/// Create a default initial state representation for a COBS encoder
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code_idx: 0,
|
||||
num_bt_sent: 1,
|
||||
offset_idx: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderState {
|
||||
/// Push a single unencoded byte into the encoder state machine
|
||||
pub fn push(&mut self, data: u8) -> PushResult {
|
||||
if data == 0 {
|
||||
let ret = PushResult::ModifyFromStartAndSkip((self.code_idx, self.num_bt_sent));
|
||||
self.code_idx += usize::from(self.offset_idx);
|
||||
self.num_bt_sent = 1;
|
||||
self.offset_idx = 1;
|
||||
ret
|
||||
} else {
|
||||
self.num_bt_sent += 1;
|
||||
self.offset_idx += 1;
|
||||
|
||||
if 0xFF == self.num_bt_sent {
|
||||
let ret = PushResult::ModifyFromStartAndPushAndSkip((
|
||||
self.code_idx,
|
||||
self.num_bt_sent,
|
||||
data,
|
||||
));
|
||||
self.num_bt_sent = 1;
|
||||
self.code_idx += usize::from(self.offset_idx);
|
||||
self.offset_idx = 1;
|
||||
ret
|
||||
} else {
|
||||
PushResult::AddSingle(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finalize the encoding process for a single message.
|
||||
/// The byte at the given index should be replaced with the given value,
|
||||
/// and the sentinel value (typically 0u8) must be inserted at the current
|
||||
/// end of the output buffer, serving as a framing byte.
|
||||
pub fn finalize(self) -> (usize, u8) {
|
||||
(self.code_idx, self.num_bt_sent)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CobsEncoder<'a> {
|
||||
/// Create a new streaming Cobs Encoder
|
||||
pub fn new(out_buf: &'a mut [u8]) -> CobsEncoder<'a> {
|
||||
CobsEncoder {
|
||||
dest: out_buf,
|
||||
dest_idx: 1,
|
||||
state: EncoderState::default(),
|
||||
might_be_done: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a slice of data to be encoded
|
||||
pub fn push(&mut self, data: &[u8]) -> Result<(), DestBufTooSmallError> {
|
||||
// TODO: could probably check if this would fit without
|
||||
// iterating through all data
|
||||
|
||||
// There was the possibility that the encoding process is done, but more data is pushed
|
||||
// instead of a `finalize` call, so the destination index needs to be incremented.
|
||||
if self.might_be_done {
|
||||
self.dest_idx += 1;
|
||||
self.might_be_done = false;
|
||||
}
|
||||
for (slice_idx, val) in data.iter().enumerate() {
|
||||
use PushResult::*;
|
||||
match self.state.push(*val) {
|
||||
AddSingle(y) => {
|
||||
*self
|
||||
.dest
|
||||
.get_mut(self.dest_idx)
|
||||
.ok_or(DestBufTooSmallError)? = y;
|
||||
}
|
||||
ModifyFromStartAndSkip((idx, mval)) => {
|
||||
*self.dest.get_mut(idx).ok_or(DestBufTooSmallError)? = mval;
|
||||
}
|
||||
ModifyFromStartAndPushAndSkip((idx, mval, nval1)) => {
|
||||
*self.dest.get_mut(idx).ok_or(DestBufTooSmallError)? = mval;
|
||||
*self
|
||||
.dest
|
||||
.get_mut(self.dest_idx)
|
||||
.ok_or(DestBufTooSmallError)? = nval1;
|
||||
// Do not increase index if these is the possibility that we are finished.
|
||||
if slice_idx == data.len() - 1 {
|
||||
// If push is called again, the index will be incremented. If finalize
|
||||
// is called, there is no need to increment the index.
|
||||
self.might_be_done = true;
|
||||
} else {
|
||||
self.dest_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All branches above require advancing the pointer at least once
|
||||
self.dest_idx += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Complete encoding of the output message. Does NOT terminate
|
||||
/// the message with the sentinel value
|
||||
pub fn finalize(self) -> usize {
|
||||
if self.dest_idx == 1 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the last index that needs to be fixed
|
||||
let (idx, mval) = self.state.finalize();
|
||||
|
||||
// If the current code index is outside of the destination slice,
|
||||
// we do not need to write it out
|
||||
if let Some(i) = self.dest.get_mut(idx) {
|
||||
*i = mval;
|
||||
}
|
||||
|
||||
self.dest_idx
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes the `source` buffer into the `dest` buffer.
|
||||
///
|
||||
/// This function assumes the typical sentinel value of 0, but does not terminate the encoded
|
||||
/// message with the sentinel value. This should be done by the caller to ensure proper framing.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of bytes written to in the `dest` buffer.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if the `dest` buffer is not large enough for the
|
||||
/// encoded message. You can calculate the size the `dest` buffer needs to be with
|
||||
/// the [crate::max_encoding_length] function.
|
||||
pub fn encode(source: &[u8], dest: &mut [u8]) -> usize {
|
||||
let mut enc = CobsEncoder::new(dest);
|
||||
enc.push(source).unwrap();
|
||||
enc.finalize()
|
||||
}
|
||||
|
||||
/// Attempts to encode the `source` buffer into the `dest` buffer.
|
||||
///
|
||||
/// This function assumes the typical sentinel value of 0, but does not terminate the encoded
|
||||
/// message with the sentinel value. This should be done by the caller to ensure proper framing.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of bytes written to in the `dest` buffer.
|
||||
///
|
||||
/// If the destination buffer does not have enough room, an error will be returned.
|
||||
pub fn try_encode(source: &[u8], dest: &mut [u8]) -> Result<usize, DestBufTooSmallError> {
|
||||
let mut enc = CobsEncoder::new(dest);
|
||||
enc.push(source)?;
|
||||
Ok(enc.finalize())
|
||||
}
|
||||
|
||||
/// Encodes the `source` buffer into the `dest` buffer using an
|
||||
/// arbitrary sentinel value.
|
||||
///
|
||||
/// This is done by first encoding the message with the typical sentinel value
|
||||
/// of 0, then XOR-ing each byte of the encoded message with the chosen sentinel
|
||||
/// value. This will ensure that the sentinel value doesn't show up in the encoded
|
||||
/// message. See the paper "Consistent Overhead Byte Stuffing" for details.
|
||||
///
|
||||
/// This function does not terminate the encoded message with the sentinel value. This should be
|
||||
/// done by the caller to ensure proper framing.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of bytes written to in the `dest` buffer.
|
||||
pub fn encode_with_sentinel(source: &[u8], dest: &mut [u8], sentinel: u8) -> usize {
|
||||
let encoded_size = encode(source, dest);
|
||||
for x in &mut dest[..encoded_size] {
|
||||
*x ^= sentinel;
|
||||
}
|
||||
encoded_size
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
/// Encodes the `source` buffer into a vector, using the [encode] function.
|
||||
pub fn encode_vec(source: &[u8]) -> alloc::vec::Vec<u8> {
|
||||
let mut encoded = alloc::vec![0; crate::max_encoding_length(source.len())];
|
||||
let encoded_len = encode(source, &mut encoded[..]);
|
||||
encoded.truncate(encoded_len);
|
||||
encoded
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
/// Encodes the `source` buffer into a vector with an arbitrary sentinel value, using the
|
||||
/// [encode_with_sentinel] function.
|
||||
pub fn encode_vec_with_sentinel(source: &[u8], sentinel: u8) -> alloc::vec::Vec<u8> {
|
||||
let mut encoded = alloc::vec![0; crate::max_encoding_length(source.len())];
|
||||
let encoded_len = encode_with_sentinel(source, &mut encoded[..], sentinel);
|
||||
encoded.truncate(encoded_len);
|
||||
encoded
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "alloc")]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn encode_target_buf_too_small() {
|
||||
let source = &[10, 11, 0, 12];
|
||||
let expected = &[3, 10, 11, 2, 12];
|
||||
for len in 0..expected.len() {
|
||||
let mut dest = alloc::vec![0; len];
|
||||
matches!(
|
||||
try_encode(source, &mut dest).unwrap_err(),
|
||||
DestBufTooSmallError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
#[should_panic]
|
||||
fn encode_target_buf_too_small_panicking() {
|
||||
let source = &[10, 11, 0, 12];
|
||||
let expected = &[3, 10, 11, 2, 12];
|
||||
encode(source, &mut alloc::vec![0; expected.len() - 1]);
|
||||
}
|
||||
}
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
//! # `cobs`
|
||||
//!
|
||||
//! This is an implementation of the Consistent Overhead Byte Stuffing (COBS) algorithm in Rust.
|
||||
//!
|
||||
//! COBS is an algorithm for transforming a message into an encoding where a specific value (the
|
||||
//! "sentinel" value) is not used. This value can then be used to mark frame boundaries in a serial
|
||||
//! communication channel.
|
||||
//!
|
||||
//! See the [wikipedia article](https://www.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) for details.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! `cobs` supports various runtime environments and is also suitable for `no_std` environments.
|
||||
//!
|
||||
//! ### Default features
|
||||
//!
|
||||
//! - [`std`](https://doc.rust-lang.org/std/): Enables functionality relying on the standard library
|
||||
//! and also activates the `alloc` feature. Currently only adds [std::error::Error] support for the
|
||||
//! library error types.
|
||||
//! - [`alloc`](https://doc.rust-lang.org/alloc/): Enables features which operate on containers
|
||||
//! like [alloc::vec::Vec](https://doc.rust-lang.org/beta/alloc/vec/struct.Vec.html).
|
||||
//! Enabled by the `std` feature.
|
||||
//!
|
||||
//! ### Optional features
|
||||
//!
|
||||
//! - [`defmt`](https://docs.rs/defmt/latest/defmt/): Adds `defmt::Format` derives on some data
|
||||
//! structures and error types.
|
||||
//! - [`serde`](https://serde.rs/): Adds `serde` derives on some data structures and error types.
|
||||
#![no_std]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#[cfg(feature = "alloc")]
|
||||
extern crate alloc;
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
// In the future, don't do this.
|
||||
mod dec;
|
||||
mod enc;
|
||||
pub use crate::dec::*;
|
||||
pub use crate::enc::*;
|
||||
|
||||
/// Calculates the maximum overhead when encoding a message with the given length.
|
||||
/// The overhead is a maximum of [n/254] bytes (one in 254 bytes) rounded up.
|
||||
#[inline]
|
||||
pub const fn max_encoding_overhead(source_len: usize) -> usize {
|
||||
source_len.div_ceil(254)
|
||||
}
|
||||
|
||||
/// Calculates the maximum possible size of an encoded message given the length
|
||||
/// of the source message. This may be useful for calculating how large the
|
||||
/// `dest` buffer needs to be in the encoding functions.
|
||||
#[inline]
|
||||
pub const fn max_encoding_length(source_len: usize) -> usize {
|
||||
source_len + max_encoding_overhead(source_len)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Usable in const context
|
||||
const ENCODED_BUF: [u8; max_encoding_length(5)] = [0; max_encoding_length(5)];
|
||||
|
||||
#[test]
|
||||
fn test_buf_len() {
|
||||
assert_eq!(ENCODED_BUF.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overhead_empty() {
|
||||
assert_eq!(max_encoding_overhead(0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overhead_one() {
|
||||
assert_eq!(max_encoding_overhead(1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overhead_larger() {
|
||||
assert_eq!(max_encoding_overhead(253), 1);
|
||||
assert_eq!(max_encoding_overhead(254), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overhead_two() {
|
||||
assert_eq!(max_encoding_overhead(255), 2);
|
||||
}
|
||||
}
|
||||
Vendored
+287
@@ -0,0 +1,287 @@
|
||||
use cobs::*;
|
||||
|
||||
fn test_pair(source: &[u8], encoded: &[u8]) {
|
||||
test_encode_decode_free_functions(source, encoded);
|
||||
test_decode_in_place(source, encoded);
|
||||
}
|
||||
|
||||
fn test_encode_decode_free_functions(source: &[u8], encoded: &[u8]) {
|
||||
let mut test_encoded = encoded.to_vec();
|
||||
let mut test_decoded = source.to_vec();
|
||||
|
||||
// Mangle data to ensure data is re-populated correctly
|
||||
test_encoded.iter_mut().for_each(|i| *i = 0x80);
|
||||
encode(source, &mut test_encoded[..]);
|
||||
|
||||
// Mangle data to ensure data is re-populated correctly
|
||||
test_decoded.iter_mut().for_each(|i| *i = 0x80);
|
||||
decode(encoded, &mut test_decoded[..]).unwrap();
|
||||
assert_eq!(encoded, test_encoded);
|
||||
assert_eq!(source, test_decoded);
|
||||
}
|
||||
|
||||
fn test_decode_in_place(source: &[u8], encoded: &[u8]) {
|
||||
let mut test_encoded = encoded.to_vec();
|
||||
let report = decode_in_place_report(&mut test_encoded).unwrap();
|
||||
assert_eq!(&test_encoded[0..report.dst_used], source);
|
||||
assert_eq!(report.src_used, encoded.len());
|
||||
|
||||
test_encoded = encoded.to_vec();
|
||||
let dst_used = decode_in_place(&mut test_encoded).unwrap();
|
||||
assert_eq!(&test_encoded[0..dst_used], source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_roundtrip() {
|
||||
for ct in 1..=1000 {
|
||||
let source: Vec<u8> = (ct..2 * ct).map(|x: usize| (x & 0xFF) as u8).collect();
|
||||
|
||||
let mut dest = vec![0u8; max_encoding_length(source.len())];
|
||||
|
||||
let sz_en = {
|
||||
let mut ce = CobsEncoder::new(&mut dest);
|
||||
|
||||
for c in source.chunks(17) {
|
||||
ce.push(c).unwrap();
|
||||
}
|
||||
ce.finalize()
|
||||
};
|
||||
|
||||
let mut decoded = source.clone();
|
||||
decoded.iter_mut().for_each(|i| *i = 0x80);
|
||||
let sz_de = {
|
||||
let mut cd = CobsDecoder::new(&mut decoded);
|
||||
|
||||
for c in dest[0..sz_en].chunks(11) {
|
||||
cd.push(c).unwrap();
|
||||
}
|
||||
|
||||
match cd.feed(0) {
|
||||
Ok(sz_msg) => sz_msg.unwrap(),
|
||||
Err(written) => panic!("decoding failed, {} bytes written to output", written),
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(sz_de, source.len());
|
||||
assert_eq!(source, decoded);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_encoding_length() {
|
||||
assert_eq!(max_encoding_length(253), 254);
|
||||
assert_eq!(max_encoding_length(254), 255);
|
||||
assert_eq!(max_encoding_length(255), 257);
|
||||
assert_eq!(max_encoding_length(254 * 2), 255 * 2);
|
||||
assert_eq!(max_encoding_length(254 * 2 + 1), 256 * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_0() {
|
||||
// An empty input is encoded as no characters.
|
||||
let mut output = [0xFFu8; 16];
|
||||
let used = encode(&[], &mut output);
|
||||
assert_eq!(used, 0);
|
||||
assert_eq!(output.as_slice(), &[0xFFu8; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_1() {
|
||||
test_pair(&[10, 11, 0, 12], &[3, 10, 11, 2, 12])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_2() {
|
||||
test_pair(&[0, 0, 1, 0], &[1, 1, 2, 1, 1])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_3() {
|
||||
test_pair(&[255, 0], &[2, 255, 1])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_4() {
|
||||
test_pair(&[1], &[2, 1])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_ex_6() {
|
||||
let mut unencoded: Vec<u8> = vec![];
|
||||
|
||||
(1..=0xFE).for_each(|i| unencoded.push(i));
|
||||
|
||||
// NOTE: trailing 0x00 is implicit
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
encoded.push(0xFF);
|
||||
(1..=0xFE).for_each(|i| encoded.push(i));
|
||||
|
||||
test_pair(&unencoded, &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_ex_7() {
|
||||
let mut unencoded: Vec<u8> = vec![];
|
||||
|
||||
(0..=0xFE).for_each(|i| unencoded.push(i));
|
||||
|
||||
// NOTE: trailing 0x00 is implicit
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
encoded.push(0x01);
|
||||
encoded.push(0xFF);
|
||||
(1..=0xFE).for_each(|i| encoded.push(i));
|
||||
|
||||
test_pair(&unencoded, &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_ex_8() {
|
||||
let mut unencoded: Vec<u8> = vec![];
|
||||
|
||||
(1..=0xFF).for_each(|i| unencoded.push(i));
|
||||
|
||||
// NOTE: trailing 0x00 is implicit
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
encoded.push(0xFF);
|
||||
(1..=0xFE).for_each(|i| encoded.push(i));
|
||||
encoded.push(0x02);
|
||||
encoded.push(0xFF);
|
||||
|
||||
test_pair(&unencoded, &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_ex_9() {
|
||||
let mut unencoded: Vec<u8> = vec![];
|
||||
|
||||
(2..=0xFF).for_each(|i| unencoded.push(i));
|
||||
unencoded.push(0x00);
|
||||
|
||||
// NOTE: trailing 0x00 is implicit
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
encoded.push(0xFF);
|
||||
(2..=0xFF).for_each(|i| encoded.push(i));
|
||||
encoded.push(0x01);
|
||||
encoded.push(0x01);
|
||||
|
||||
test_pair(&unencoded, &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wikipedia_ex_10() {
|
||||
let mut unencoded: Vec<u8> = vec![];
|
||||
|
||||
(3..=0xFF).for_each(|i| unencoded.push(i));
|
||||
unencoded.push(0x00);
|
||||
unencoded.push(0x01);
|
||||
|
||||
// NOTE: trailing 0x00 is implicit
|
||||
let mut encoded: Vec<u8> = vec![];
|
||||
encoded.push(0xFE);
|
||||
(3..=0xFF).for_each(|i| encoded.push(i));
|
||||
encoded.push(0x02);
|
||||
encoded.push(0x01);
|
||||
|
||||
test_pair(&unencoded, &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_15() {
|
||||
// Reported: https://github.com/awelkie/cobs.rs/issues/15
|
||||
|
||||
let my_string_buf = b"\x00\x11\x00\x22";
|
||||
let max_len = max_encoding_length(my_string_buf.len());
|
||||
assert!(max_len < 128);
|
||||
let mut buf = [0u8; 128];
|
||||
|
||||
let len = encode_with_sentinel(my_string_buf, &mut buf, b'\x00');
|
||||
|
||||
let cobs_buf = &buf[0..len];
|
||||
|
||||
let mut decoded_dest_buf = [0u8; 128];
|
||||
let new_len = decode_with_sentinel(cobs_buf, &mut decoded_dest_buf, b'\x00').unwrap();
|
||||
let decoded_buf = &decoded_dest_buf[0..new_len];
|
||||
|
||||
println!("{:?} {:?} {:?}", my_string_buf, cobs_buf, decoded_buf);
|
||||
assert_eq!(my_string_buf, decoded_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_19_test_254_block_all_ones() {
|
||||
let src: [u8; 254] = [1; 254];
|
||||
let mut dest: [u8; 256] = [0; 256];
|
||||
let encode_len = encode(&src, &mut dest);
|
||||
assert_eq!(encode_len, 255);
|
||||
let mut decoded: [u8; 254] = [1; 254];
|
||||
let decoded_len = decode(&dest, &mut decoded).expect("decoding failed");
|
||||
assert_eq!(decoded_len, 254);
|
||||
assert_eq!(&src, &decoded);
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
mod alloc_tests {
|
||||
use super::*;
|
||||
use quickcheck::{quickcheck, TestResult};
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_1() {
|
||||
test_roundtrip(&[1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_2() {
|
||||
for i in 0..5usize {
|
||||
let mut v = Vec::new();
|
||||
for j in 0..252 + i {
|
||||
v.push(j as u8);
|
||||
}
|
||||
test_roundtrip(&v);
|
||||
}
|
||||
}
|
||||
|
||||
fn identity(source: Vec<u8>, sentinel: u8) -> TestResult {
|
||||
let encoded = encode_vec_with_sentinel(&source[..], sentinel);
|
||||
|
||||
if source.is_empty() {
|
||||
return TestResult::passed();
|
||||
}
|
||||
|
||||
// Check that the sentinel doesn't show up in the encoded message
|
||||
for x in encoded.iter() {
|
||||
if *x == sentinel {
|
||||
return TestResult::error("Sentinel found in encoded message.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the decoding the encoded message returns the original message
|
||||
match decode_vec_with_sentinel(&encoded[..], sentinel) {
|
||||
Ok(decoded) => {
|
||||
if source == decoded {
|
||||
TestResult::passed()
|
||||
} else {
|
||||
TestResult::failed()
|
||||
}
|
||||
}
|
||||
Err(_) => TestResult::error("decoding Error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_with_sentinel() {
|
||||
quickcheck(identity as fn(Vec<u8>, u8) -> TestResult);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode() {
|
||||
fn identity_default_sentinel(source: Vec<u8>) -> TestResult {
|
||||
identity(source, 0)
|
||||
}
|
||||
quickcheck(identity_default_sentinel as fn(Vec<u8>) -> TestResult);
|
||||
}
|
||||
|
||||
fn test_roundtrip(source: &[u8]) {
|
||||
let encoded = encode_vec(source);
|
||||
let decoded = decode_vec(&encoded).expect("decode_vec");
|
||||
assert_eq!(source, decoded);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user