From aea661c7a0d12d59e6b9515fcddd2ce86288bd5d Mon Sep 17 00:00:00 2001 From: Victor Koenders Date: Sun, 15 Oct 2023 15:29:23 +0200 Subject: [PATCH] Started working on adding embedded-io support --- .github/workflows/rust.yml | 417 ++++++++++++++----------------- Cargo.toml | 1 + src/error.rs | 10 + src/features/impl_embedded_io.rs | 127 ++++++++++ src/features/mod.rs | 5 + src/lib.rs | 15 +- 6 files changed, 344 insertions(+), 231 deletions(-) create mode 100644 src/features/impl_embedded_io.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 53af7bb0..6d44ca64 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,234 +1,203 @@ { "name": "CI", - "on": { - "push": { - "branches": [ - "trunk", - "v*.x", - "ci/*" - ] + "on": + { + "push": { "branches": ["trunk", "v*.x", "ci/*"] }, + "pull_request": { "branches": ["trunk", "v*.x"] }, }, - "pull_request": { - "branches": [ - "trunk", - "v*.x" - ] - } - }, - "jobs": { - "check": { - "name": "Check", - "runs-on": "ubuntu-latest", - "strategy": { - "fail-fast": false, - "matrix": { - "rust": [ - "stable", - "beta", - "nightly" - # "1.55.0" TODO: Pick latest stable version when we release 2.0 - ] - } - }, - "steps": [ - { - "uses": "actions/checkout@v4", - "name": "Checkout" + "jobs": + { + "check": + { + "name": "Check", + "runs-on": "ubuntu-latest", + "strategy": { "fail-fast": false, "matrix": { "rust": [ + "stable", + "beta", + "nightly", + # "1.55.0" TODO: Pick latest stable version when we release 2.0 + ] } }, + "steps": + [ + { "uses": "actions/checkout@v4", "name": "Checkout" }, + { + "uses": "actions-rs/toolchain@v1", + "with": + { + "profile": "minimal", + "toolchain": "${{ matrix.rust }}", + "override": true, + }, + "name": "Install Rust ${{ matrix.rust }}", + }, + { + "uses": "actions-rs/cargo@v1", + "with": { "command": "check", "args": "--all-features" }, + "name": "Run `cargo check`", + }, + { + "uses": "actions-rs/cargo@v1", + "with": { "command": "check", "args": "--bench *" }, + "name": "Run `cargo check` on benches", + }, + { + "uses": "actions-rs/cargo@v1", + "with": { "command": "check", "args": "--examples" }, + "name": "Check examples", + }, + ], }, - { - "uses": "actions-rs/toolchain@v1", - "with": { - "profile": "minimal", - "toolchain": "${{ matrix.rust }}", - "override": true - }, - "name": "Install Rust ${{ matrix.rust }}" + "test": + { + "name": "Test", + "strategy": + { + "matrix": + { + "runner": ["ubuntu-latest", "windows-latest", "macos-latest"], + "rust": [ + "stable", + # "1.55.0" TODO: Pick latest stable version when we release 2.0 + ], + "features": + [ + "", + "alloc", + "alloc,derive", + "std", + "std,derive", + "serde", + "alloc,serde", + "std,serde", + "serde,derive", + "alloc,serde,derive", + "std,serde,derive", + "embedded-io", + "embedded-io,alloc", + "embedded-io,std", + "embedded-io,serde", + "embedded-io,serde,alloc", + "embedded-io,serde,std", + ], + }, + }, + "runs-on": "${{ matrix.runner }}", + "steps": + [ + { "uses": "actions/checkout@v4", "name": "Checkout" }, + { + "uses": "actions-rs/toolchain@v1", + "with": + { + "profile": "minimal", + "toolchain": "${{ matrix.rust }}", + "override": true, + }, + "name": "Install Rust ${{ matrix.rust }}", + }, + { "run": "if [ -z \"${{ matrix.features }}\" ]\n + then\n + cargo test --no-default-features\n + else\n + cargo test --no-default-features --features ${{ matrix.features }}\n + fi", "name": "Run `cargo test` on all features", "shell": "bash", "env": { "RUSTFLAGS": "-D warnings" } }, + ], }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "check", - "args": "--all-features" - }, - "name": "Run `cargo check`" + "lints": + { + "name": "Lints", + "runs-on": "ubuntu-latest", + "steps": + [ + { "uses": "actions/checkout@v4", "name": "Checkout" }, + { + "uses": "actions-rs/toolchain@v1", + "with": + { + "profile": "minimal", + "toolchain": "stable", + "override": true, + "components": "rustfmt, clippy", + }, + "name": "Install Rust stable", + }, + { + "uses": "actions-rs/cargo@v1", + "with": { "command": "fmt", "args": "--all -- --check" }, + "name": "Run `cargo fmt`", + }, + { + "uses": "actions-rs/cargo@v1", + "with": + { + "command": "clippy", + "args": "--all-features -- -D warnings", + }, + "name": "Run `cargo clippy`", + }, + ], }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "check", - "args": "--bench *" - }, - "name": "Run `cargo check` on benches" + "compatibility": + { + "name": "Compatibility", + "runs-on": "ubuntu-latest", + "steps": + [ + { "uses": "actions/checkout@v4", "name": "Checkout" }, + { + "uses": "actions-rs/toolchain@v1", + "with": + { + "profile": "minimal", + "toolchain": "stable", + "override": true, + }, + "name": "Install Rust stable", + }, + { + "uses": "actions-rs/cargo@v1", + "with": + { + "command": "test", + "args": "--manifest-path compatibility/Cargo.toml", + }, + "name": "Run compatibility tests", + }, + ], }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "check", - "args": "--examples" - }, - "name": "Check examples" - } - ] - }, - "test": { - "name": "Test", - "strategy": { - "matrix": { - "runner": [ - "ubuntu-latest", - "windows-latest", - "macos-latest" - ], - "rust": [ - "stable", - # "1.55.0" TODO: Pick latest stable version when we release 2.0 - ], - "features": [ - "", - "alloc", - "alloc,derive", - "std", - "std,derive", - "serde", - "alloc,serde", - "std,serde", - "serde,derive", - "alloc,serde,derive", - "std,serde,derive", - ] - } - }, - "runs-on": "${{ matrix.runner }}", - "steps": [ - { - "uses": "actions/checkout@v4", - "name": "Checkout" - }, - { - "uses": "actions-rs/toolchain@v1", - "with": { - "profile": "minimal", - "toolchain": "${{ matrix.rust }}", - "override": true - }, - "name": "Install Rust ${{ matrix.rust }}" + "coverage": + { + "name": "Code Coverage", + "runs-on": "ubuntu-latest", + "steps": + [ + { "uses": "actions/checkout@v4", "name": "Checkout" }, + { + "uses": "actions-rs/toolchain@v1", + "with": + { + "profile": "minimal", + "toolchain": "nightly", + "override": true, + }, + "name": "Install Rust nightly", + }, + { + "name": "Run cargo-tarpaulin", + "uses": "actions-rs/tarpaulin@v0.1", + "with": { "version": "0.19.1", "args": "--all --all-features" }, + }, + { + "name": "Upload to codecov.io", + "uses": "codecov/codecov-action@v3", + }, + { + "name": "Archive code coverage results", + "uses": "actions/upload-artifact@v3", + "with": + { "name": "code-coverage-report", "path": "cobertura.xml" }, + }, + ], }, - { - "run": "if [ -z \"${{ matrix.features }}\" ]\n -then\n - cargo test --no-default-features\n -else\n - cargo test --no-default-features --features ${{ matrix.features }}\n -fi", - "name": "Run `cargo test` on all features", - "shell": "bash", - "env": { - "RUSTFLAGS": "-D warnings" - } - } - ] }, - "lints": { - "name": "Lints", - "runs-on": "ubuntu-latest", - "steps": [ - { - "uses": "actions/checkout@v4", - "name": "Checkout" - }, - { - "uses": "actions-rs/toolchain@v1", - "with": { - "profile": "minimal", - "toolchain": "stable", - "override": true, - "components": "rustfmt, clippy" - }, - "name": "Install Rust stable" - }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "fmt", - "args": "--all -- --check" - }, - "name": "Run `cargo fmt`" - }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "clippy", - "args": "--all-features -- -D warnings" - }, - "name": "Run `cargo clippy`" - } - ] - }, - "compatibility": { - "name": "Compatibility", - "runs-on": "ubuntu-latest", - "steps": [ - { - "uses": "actions/checkout@v4", - "name": "Checkout" - }, - { - "uses": "actions-rs/toolchain@v1", - "with": { - "profile": "minimal", - "toolchain": "stable", - "override": true, - }, - "name": "Install Rust stable" - }, - { - "uses": "actions-rs/cargo@v1", - "with": { - "command": "test", - "args": "--manifest-path compatibility/Cargo.toml" - }, - "name": "Run compatibility tests" - } - ] - }, - "coverage": { - "name": "Code Coverage", - "runs-on": "ubuntu-latest", - "steps": [ - { - "uses": "actions/checkout@v4", - "name": "Checkout" - }, - { - "uses": "actions-rs/toolchain@v1", - "with": { - "profile": "minimal", - "toolchain": "nightly", - "override": true - }, - "name": "Install Rust nightly" - }, - { - "name": "Run cargo-tarpaulin", - "uses": "actions-rs/tarpaulin@v0.1", - "with": { - "version": "0.19.1", - "args": "--all --all-features" - } - }, - { - "name": "Upload to codecov.io", - "uses": "codecov/codecov-action@v3" - }, - { - "name": "Archive code coverage results", - "uses": "actions/upload-artifact@v3", - "with": { - "name": "code-coverage-report", - "path": "cobertura.xml" - } - } - ] - } - } } diff --git a/Cargo.toml b/Cargo.toml index 256e2c9a..83410894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ derive = ["bincode_derive"] bincode_derive = { path = "derive", version = "2.0.0-rc.3", optional = true } serde = { version = "1.0", default-features = false, optional = true } unty = "0.0.3" +embedded-io = { version = "0.6", default-features = false, optional = true } # Used for tests [dev-dependencies] diff --git a/src/error.rs b/src/error.rs index 8c6ece4e..3885b823 100644 --- a/src/error.rs +++ b/src/error.rs @@ -54,6 +54,11 @@ pub enum EncodeError { #[cfg(feature = "serde")] /// A serde-specific error that occurred while decoding. Serde(crate::features::serde::EncodeError), + + #[cfg(feature = "embedded-io")] + /// An embedded-io error happened while encoding or decoding + // TODO: We lose the inner error here, but it's generic based on the reader. it would be nice to add this + EmbeddedIo, } impl core::fmt::Display for EncodeError { @@ -188,6 +193,11 @@ pub enum DecodeError { #[cfg(feature = "serde")] /// A serde-specific error that occurred while decoding. Serde(crate::features::serde::DecodeError), + + #[cfg(feature = "embedded-io")] + /// An embedded-io error happened while encoding or decoding + // TODO: We lose the inner error here, but it's generic based on the reader. it would be nice to add this + EmbeddedIo, } impl core::fmt::Display for DecodeError { diff --git a/src/features/impl_embedded_io.rs b/src/features/impl_embedded_io.rs new file mode 100644 index 00000000..91a7d6b7 --- /dev/null +++ b/src/features/impl_embedded_io.rs @@ -0,0 +1,127 @@ +//! Functions that work with `embedded-io`'s traits. +//! +//! |Trait name|Function name| +//! |----------|-------------| +//! |[`embedded_io::Write`]|[`encode_into_write`]| +//! |[`embedded_io::Read`]|[`decode_from_read`]| +//! |[`embedded_io::BufRead`]|[`decode_from_buf_read`]| +//! +//! Note that all these functions currently block. There is no partial decoding or waiting for a reader/writer to be ready with [`embedded_io::ReadReady`] or [`embedded_io::WriteReady`]. + +use crate::{ + config::Config, + de::DecoderImpl, + enc::EncoderImpl, + error::{DecodeError, EncodeError}, + Decode, Encode, +}; + +/// Encode the given value into a [`embedded_io::Write`] with the given `Config`. See the [config] module for more information. +/// +/// [config]: ../config/index.html +#[cfg_attr(docsrs, doc(cfg(feature = "embedded-io")))] +pub fn encode_into_write( + val: E, + dst: &mut W, + config: C, +) -> Result { + let writer = IoWriter { + writer: dst, + bytes_written: 0, + }; + let mut encoder = EncoderImpl::<_, C>::new(writer, config); + val.encode(&mut encoder)?; + Ok(encoder.into_writer().bytes_written) +} + +struct IoWriter<'a, W: embedded_io::Write> { + writer: &'a mut W, + bytes_written: usize, +} + +impl<'a, W> crate::enc::write::Writer for IoWriter<'a, W> +where + W: embedded_io::Write, +{ + fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> { + self.writer + .write_all(bytes) + .map_err(|_| EncodeError::EmbeddedIo)?; + self.bytes_written += bytes.len(); + Ok(()) + } +} + +/// Decode type `D` from the given reader with the given `Config`. The reader can be any type that implements [`embedded_io::Read`]. +/// +/// See the [config] module for more information about config options. +/// +/// [config]: ../config/index.html +#[cfg_attr(docsrs, doc(cfg(feature = "embedded-io")))] +pub fn decode_from_read( + src: &mut R, + config: C, +) -> Result { + let reader = IoRead { reader: src }; + let mut decoder = DecoderImpl::<_, C>::new(reader, config); + D::decode(&mut decoder) +} + +struct IoRead<'a, R: embedded_io::Read> { + reader: &'a mut R, +} + +impl<'a, R: embedded_io::Read> crate::de::read::Reader for IoRead<'a, R> { + fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> { + self.reader + .read_exact(bytes) + .map_err(|_| DecodeError::EmbeddedIo) + } +} + +/// Decode type `D` from the given reader with the given `Config`. The reader can be any type that implements [`embedded_io::BufRead`]. +/// +/// See the [config] module for more information about config options. +/// +/// [config]: ../config/index.html +#[cfg_attr(docsrs, doc(cfg(feature = "embedded-io")))] +pub fn decode_from_buf_read( + src: &mut R, + config: C, +) -> Result { + let reader = IoBufRead { reader: src }; + let mut decoder = DecoderImpl::<_, C>::new(reader, config); + D::decode(&mut decoder) +} + +struct IoBufRead<'a, R: embedded_io::BufRead> { + reader: &'a mut R, +} + +impl<'a, R: embedded_io::BufRead> crate::de::read::Reader for IoBufRead<'a, R> { + fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> { + let mut buf = self + .reader + .fill_buf() + .map_err(|_| DecodeError::EmbeddedIo)?; + let mut last_len = buf.len(); + + // loop to read more until we have enough + while buf.len() < bytes.len() { + buf = self + .reader + .fill_buf() + .map_err(|_| DecodeError::EmbeddedIo)?; + // Detect infinite loops where 0 bytes get read + if buf.len() == last_len { + return Err(DecodeError::EmbeddedIo); + } + last_len = buf.len(); + } + + bytes.copy_from_slice(&buf[..bytes.len()]); + self.reader.consume(bytes.len()); + + Ok(()) + } +} diff --git a/src/features/mod.rs b/src/features/mod.rs index 16a67e11..14346d46 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -16,3 +16,8 @@ pub use self::derive::*; #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] pub mod serde; + +#[cfg(feature = "embedded-io")] +#[cfg_attr(docsrs, doc(cfg(feature = "embedded-io")))] +#[path = "impl_embedded_io.rs"] +pub mod embedded_io; diff --git a/src/lib.rs b/src/lib.rs index 3c225eee..40868740 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,13 +15,14 @@ //! //! # Features //! -//! |Name |Default?|Supported types for Encode/Decode|Enabled methods |Other| -//! |------|--------|-----------------------------------------|-----------------------------------------------------------------|-----| -//! |std | Yes |`HashMap` and `HashSet`|`decode_from_std_read` and `encode_into_std_write`| -//! |alloc | Yes |All common containers in alloc, like `Vec`, `String`, `Box`|`encode_to_vec`| -//! |atomic| Yes |All `Atomic*` integer types, e.g. `AtomicUsize`, and `AtomicBool`|| -//! |derive| Yes |||Enables the `BorrowDecode`, `Decode` and `Encode` derive macros| -//! |serde | No |`Compat` and `BorrowCompat`, which will work for all types that implement serde's traits|serde-specific encode/decode functions in the [serde] module|Note: There are several [known issues](serde/index.html#known-issues) when using serde and bincode| +//! |Name |Default?|Supported types for Encode/Decode|Enabled methods |Other| +//! |------------|--------|-----------------------------------------|-----------------------------------------------------------------|-----| +//! |std | Yes |`HashMap` and `HashSet`|`decode_from_std_read` and `encode_into_std_write`| +//! |alloc | Yes |All common containers in alloc, like `Vec`, `String`, `Box`|`encode_to_vec`| +//! |atomic | Yes |All `Atomic*` integer types, e.g. `AtomicUsize`, and `AtomicBool`|| +//! |derive | Yes |||Enables the `BorrowDecode`, `Decode` and `Encode` derive macros| +//! |serde | No |`Compat` and `BorrowCompat`, which will work for all types that implement serde's traits|serde-specific encode/decode functions in the [serde] module|Note: There are several [known issues](serde/index.html#known-issues) when using serde and bincode| +//! |embedded-io | No ||`embedded-io`-specific encode/decode functions in the [embedded_io] module| //! //! # Which functions to use //!