From d747df47c027a0db2daac44e930646e870ee513a Mon Sep 17 00:00:00 2001 From: Georges Palauqui Date: Fri, 25 Aug 2023 16:52:11 +0200 Subject: [PATCH 1/2] Add support for heapless feature to avoid alloc in no_std case --- .github/workflows/build.yml | 28 +++++ .gitlab-ci.yml | 1 + Cargo.toml | 4 + README.md | 2 + src/lib.rs | 207 ++++++++++++++++++++++++++++++++++++ src/serde.rs | 55 +++++++++- tests/serde-heapless.rs | 64 +++++++++++ 7 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 tests/serde-heapless.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d63581e..e080b46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,12 @@ jobs: command: test args: --verbose --no-default-features --features alloc + - name: Test [heapless] + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --no-default-features --features heapless + - name: Test [serde] uses: actions-rs/cargo@v1 with: @@ -89,6 +95,28 @@ jobs: command: build args: --no-default-features --features alloc --target thumbv6m-none-eabi + build-no-alloc: + name: Build no_std + runs-on: ubuntu-latest + + steps: + - name: Checkout Sources + uses: actions/checkout@v2 + + - name: Install Rust Toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + target: thumbv6m-none-eabi + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --no-default-features --features heapless --target thumbv6m-none-eabi + # coverage: # name: Code Coverage # runs-on: ubuntu-latest diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index afa6710..95ad350 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,6 +30,7 @@ test: - cargo test --no-default-features --features alloc - cargo test --no-default-features --features std - cargo test --no-default-features --features serde + - cargo test --no-default-features --features heapless cache: key: "$CI_COMMIT_REF_SLUG:$RUST_VERSION" paths: diff --git a/Cargo.toml b/Cargo.toml index 85e4956..3085d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,15 @@ maintenance = { status = "actively-developed" } default = ["std"] alloc = [] std = ["alloc"] +serde = ["dep:serde", "heapless?/serde"] +heapless = ["dep:heapless"] [[bench]] name = "hex" harness = false [dependencies] +heapless = { version = "0.7", optional = true } serde = { version = "1.0", default-features = false, optional = true } [dev-dependencies] @@ -36,6 +39,7 @@ version-sync = "0.9.5" pretty_assertions = "1.4.1" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" +serde-json-core = "0.6" [package.metadata.docs.rs] all-features = true diff --git a/README.md b/README.md index 7d0e67f..f6688ce 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ hex = { version = "0.4", default-features = false } Enabled by default. Add support for Rust's libstd types. - `alloc`: Enabled by default. Add support for alloc types (e.g. `String`) in `no_std` environment. +- `heapless`: + Disabled by default. Add support for heapless types (e.g. `String`) in `no_std` environment with allocation on the stack. - `serde`: Disabled by default. Add support for `serde` de/serializing library. See the `serde` module documentation for usage. diff --git a/src/lib.rs b/src/lib.rs index 5a2b93f..76766b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,8 @@ pub mod serde; pub use crate::serde::deserialize; #[cfg(all(feature = "alloc", feature = "serde"))] pub use crate::serde::{serialize, serialize_upper}; +#[cfg(all(feature = "heapless", feature = "serde"))] +pub use crate::serde::{serialize_heapless, serialize_upper_heapless}; /// Encoding values as hex string. /// @@ -196,6 +198,23 @@ impl FromHex for Vec { } } +#[cfg(feature = "heapless")] +impl FromHex for heapless::Vec { + type Error = FromHexError; + + fn from_hex>(hex: T) -> Result { + let hex = hex.as_ref(); + if hex.len() % 2 != 0 { + return Err(FromHexError::OddLength); + } + + hex.chunks(2) + .enumerate() + .map(|(i, pair)| Ok(val(pair[0], 2 * i)? << 4 | val(pair[1], 2 * i + 1)?)) + .collect() + } +} + impl FromHex for [u8; N] { type Error = FromHexError; @@ -226,6 +245,42 @@ pub fn encode>(data: T) -> String { data.encode_hex() } +/// Encodes `data` as hex string using lowercase characters. +/// +/// Lowercase characters are used (e.g. `f9b4ca`). The resulting string's +/// length is always even, each byte in `data` is always encoded using two hex +/// digits. Thus, the resulting string contains exactly twice as many bytes as +/// the input data. +/// +/// Because it use heapless::String, this function need type annotation. +/// The explicit type String, has to be able to hold at least `data.len() * 2` bytes, +/// otherwise this function will return an error. +/// +/// # Example +/// +/// ``` +/// use heapless::{String, Vec}; +/// +/// let hex_str: String<24> = hex::encode_heapless("Hello world!").expect("encode error"); +/// assert_eq!(hex_str, "48656c6c6f20776f726c6421"); +/// let hex_str: String<10> = hex::encode_heapless(Vec::::from_slice(&[1, 2, 3, 15, 16]).unwrap()).expect("encode error"); +/// assert_eq!(hex_str, "0102030f10"); +/// let hex_str: String<30> = hex::encode_heapless("can be longer").expect("encode error"); +/// assert_eq!(hex_str, "63616e206265206c6f6e676572"); +/// let res: Result,_> = hex::encode_heapless("but not shorter"); +/// assert!(res.is_err()); +/// ``` +#[must_use] +#[cfg(feature = "heapless")] +pub fn encode_heapless, const N: usize>( + data: T, +) -> Result, FromHexError> { + if data.as_ref().len() * 2 > N { + return Err(FromHexError::InvalidStringLength); + } + Ok(data.encode_hex()) +} + /// Encodes `data` as hex string using uppercase characters. /// /// Apart from the characters' casing, this works exactly like `encode()`. @@ -242,6 +297,34 @@ pub fn encode_upper>(data: T) -> String { data.encode_hex_upper() } +/// Encodes `data` as hex string using uppercase characters. +/// +/// Apart from the characters' casing, this works exactly like `encode_heapless()`. +/// +/// # Example +/// +/// ``` +/// use heapless::{String, Vec}; +/// +/// let hex_str: String<24> = hex::encode_upper_heapless("Hello world!").expect("encode error"); +/// assert_eq!(hex_str, "48656C6C6F20776F726C6421"); +/// let hex_str: String<10> = hex::encode_upper_heapless(Vec::::from_slice(&[1, 2, 3, 15, 16]).unwrap()).expect("encode error"); +/// assert_eq!(hex_str, "0102030F10"); +/// let hex_str: String<30> = hex::encode_upper_heapless("can be longer").expect("encode error"); +/// assert_eq!(hex_str, "63616E206265206C6F6E676572"); +/// let res: Result,_> = hex::encode_upper_heapless("but not shorter"); +/// assert!(res.is_err()); +/// ``` +#[cfg(feature = "heapless")] +pub fn encode_upper_heapless, const N: usize>( + data: T, +) -> Result, FromHexError> { + if data.as_ref().len() * 2 > N { + return Err(FromHexError::InvalidStringLength); + } + Ok(data.encode_hex_upper()) +} + /// Decodes a hex string into raw bytes. /// /// Both, upper and lower case characters are valid in the input string and can @@ -263,6 +346,32 @@ pub fn decode>(data: T) -> Result, FromHexError> { FromHex::from_hex(data) } +/// Decodes a hex string into raw bytes. +/// +/// Both, upper and lower case characters are valid in the input string and can +/// even be mixed (e.g. `f9b4ca`, `F9B4CA` and `f9B4Ca` are all valid strings). +/// +/// # Example +/// +/// ``` +/// use heapless::Vec; +/// +/// let res: Vec = hex::decode_heapless("48656c6c6f20776f726c6421").expect("decode error"); +/// let expected: Vec = Vec::from_slice(b"Hello world!").unwrap(); +/// assert_eq!(res, expected); +/// +/// let res: Result,_> = hex::decode_heapless("123"); +/// assert_eq!(res, Err(hex::FromHexError::OddLength)); +/// let res: Result,_> = hex::decode_heapless("foo"); +/// assert!(res.is_err()); +/// ``` +#[cfg(feature = "heapless")] +pub fn decode_heapless, const N: usize>( + data: T, +) -> Result, FromHexError> { + FromHex::from_hex(data) +} + /// Decode a hex string into a mutable bytes slice. /// /// Both, upper and lower case characters are valid in the input string and can @@ -423,6 +532,15 @@ mod test { assert_eq!(encode("foobar"), "666f6f626172"); } + #[test] + #[cfg(feature = "heapless")] + fn test_encode_heapless() { + assert_eq!( + encode_heapless::<&str, 12>("foobar").expect("encode error"), + "666f6f626172" + ); + } + #[test] #[cfg(feature = "alloc")] fn test_decode() { @@ -432,6 +550,15 @@ mod test { ); } + #[test] + #[cfg(feature = "heapless")] + fn test_decode_heapless() { + assert_eq!( + decode_heapless::<&str, 12>("666f6f626172"), + Ok(heapless::Vec::from_slice(b"foobar").unwrap()) + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_from_hex_okay_str() { @@ -439,6 +566,19 @@ mod test { assert_eq!(Vec::from_hex("666F6F626172").unwrap(), b"foobar"); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_from_hex_okay_str_heapless() { + assert_eq!( + as FromHex>::from_hex("666f6f626172").unwrap(), + b"foobar" + ); + assert_eq!( + as FromHex>::from_hex("666F6F626172").unwrap(), + b"foobar" + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_from_hex_okay_bytes() { @@ -446,6 +586,19 @@ mod test { assert_eq!(Vec::from_hex(b"666F6F626172").unwrap(), b"foobar"); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_from_hex_okay_bytes_heapless() { + assert_eq!( + as FromHex>::from_hex(b"666f6f626172").unwrap(), + b"foobar" + ); + assert_eq!( + as FromHex>::from_hex(b"666F6F626172").unwrap(), + b"foobar" + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_invalid_length() { @@ -456,6 +609,19 @@ mod test { ); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_invalid_length_heapless() { + assert_eq!( + as FromHex>::from_hex("1").unwrap_err(), + FromHexError::OddLength + ); + assert_eq!( + as FromHex>::from_hex("666f6f6261721").unwrap_err(), + FromHexError::OddLength + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_invalid_char() { @@ -465,12 +631,30 @@ mod test { ); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_invalid_char_heapless() { + assert_eq!( + as FromHex>::from_hex("66ag").unwrap_err(), + FromHexError::InvalidHexCharacter { c: 'g', index: 3 } + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_empty() { assert_eq!(Vec::from_hex("").unwrap(), b""); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_empty_heapless() { + assert_eq!( + as FromHex>::from_hex("").unwrap(), + b"" + ); + } + #[test] #[cfg(feature = "alloc")] pub fn test_from_hex_whitespace() { @@ -480,6 +664,15 @@ mod test { ); } + #[test] + #[cfg(feature = "heapless")] + pub fn test_from_hex_whitespace_heapless() { + assert_eq!( + as FromHex>::from_hex("666f 6f62617").unwrap_err(), + FromHexError::InvalidHexCharacter { c: ' ', index: 4 } + ); + } + #[test] pub fn test_from_hex_array() { assert_eq!( @@ -506,4 +699,18 @@ mod test { "666F6F626172".to_string(), ); } + + #[test] + #[cfg(feature = "heapless")] + fn test_to_hex_heapless() { + assert_eq!( + [0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72].encode_hex::>(), + heapless::String::<12>::from("666f6f626172"), + ); + + assert_eq!( + [0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72].encode_hex_upper::>(), + heapless::String::<12>::from("666F6F626172"), + ); + } } diff --git a/src/serde.rs b/src/serde.rs index 773f298..cb79c0f 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -16,9 +16,28 @@ struct Foo { ``` "## )] +#[cfg_attr( + all(feature = "heapless", feature = "serde"), + doc = r##" +# Example + +``` +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +struct Foo { + #[serde( + serialize_with = "hex::serialize_heapless::<_, _, N>", + deserialize_with = "hex::deserialize" + )] + bar: heapless::Vec, +} +``` +"## +)] use serde::de::{Error, Visitor}; use serde::Deserializer; -#[cfg(feature = "alloc")] +#[cfg(any(feature = "alloc", feature = "heapless"))] use serde::Serializer; #[cfg(feature = "alloc")] @@ -29,7 +48,7 @@ use core::marker::PhantomData; use crate::FromHex; -#[cfg(feature = "alloc")] +#[cfg(any(feature = "alloc", feature = "heapless"))] use crate::ToHex; /// Serializes `data` as hex string using uppercase characters. @@ -45,6 +64,22 @@ where serializer.serialize_str(&s) } +/// Serializes `data` as hex string using uppercase characters. +/// +/// Apart from the characters' casing, this works exactly like `serialize_heapless()`. +#[cfg(feature = "heapless")] +pub fn serialize_upper_heapless( + data: T, + serializer: S, +) -> Result +where + S: Serializer, + T: ToHex, +{ + let s = data.encode_hex_upper::>(); + serializer.serialize_str(&s) +} + /// Serializes `data` as hex string using lowercase characters. /// /// Lowercase characters are used (e.g. `f9b4ca`). The resulting string's length @@ -61,6 +96,22 @@ where serializer.serialize_str(&s) } +/// Serializes `data` as hex string using lowercase characters. +/// +/// Lowercase characters are used (e.g. `f9b4ca`). The resulting string's length +/// is always even, each byte in data is always encoded using two hex digits. +/// Thus, the resulting string contains exactly twice as many bytes as the input +/// data. +#[cfg(feature = "heapless")] +pub fn serialize_heapless(data: T, serializer: S) -> Result +where + S: Serializer, + T: ToHex, +{ + let s = data.encode_hex::>(); + serializer.serialize_str(&s) +} + /// Deserializes a hex string into raw bytes. /// /// Both, upper and lower case characters are valid in the input string and can diff --git a/tests/serde-heapless.rs b/tests/serde-heapless.rs new file mode 100644 index 0000000..e2c19ac --- /dev/null +++ b/tests/serde-heapless.rs @@ -0,0 +1,64 @@ +#![cfg(all(feature = "serde", feature = "heapless"))] +#![allow(clippy::blacklisted_name)] + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Foo { + #[serde( + serialize_with = "hex::serialize_heapless::<_, _, N>", + deserialize_with = "hex::deserialize" + )] + bar: heapless::Vec, +} + +#[test] +fn serialize_heapless() { + let foo: Foo<8> = Foo { + bar: heapless::Vec::from_slice(&[1, 10, 100, 1]).unwrap(), + }; + + let ser = serde_json_core::to_string::<_, 18>(&foo).expect("serialization failed"); + assert_eq!(ser, r#"{"bar":"010a6401"}"#); +} + +#[test] +fn deserialize_heapless() { + let foo = Foo { + bar: heapless::Vec::from_slice(&[1, 10, 100, 1]).unwrap(), + }; + + let (de, _): (Foo<8>, usize) = + serde_json_core::from_str(r#"{"bar":"010a6401"}"#).expect("deserialization failed"); + assert_eq!(de, foo); +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +struct Bar { + #[serde( + serialize_with = "hex::serialize_upper_heapless::<_, _, N>", + deserialize_with = "hex::deserialize" + )] + foo: heapless::Vec, +} + +#[test] +fn serialize_upper_heapless() { + let bar: Bar<6> = Bar { + foo: heapless::Vec::from_slice(&[1, 10, 100]).unwrap(), + }; + + let ser = serde_json_core::to_string::<_, 16>(&bar).expect("serialization failed"); + assert_eq!(ser, r#"{"foo":"010A64"}"#); +} + +#[test] +fn deserialize_upper_heapless() { + let bar = Bar { + foo: heapless::Vec::from_slice(&[1, 10, 100]).unwrap(), + }; + + let (de, _): (Bar<3>, usize) = + serde_json_core::from_str(r#"{"foo":"010A64"}"#).expect("deserialization failed"); + assert_eq!(de, bar); +} From ea988fb41138e69335f60bf851330a001bed4fff Mon Sep 17 00:00:00 2001 From: Georges Palauqui Date: Wed, 31 Jul 2024 14:54:41 +0200 Subject: [PATCH 2/2] bump heapless to 0.8 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3085d94..989d3e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ name = "hex" harness = false [dependencies] -heapless = { version = "0.7", optional = true } +heapless = { version = "0.8", optional = true } serde = { version = "1.0", default-features = false, optional = true } [dev-dependencies]