diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9aa36ce..0adc3a4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,135 +2,79 @@ name: CI on: push: - branches: - - main - - develop - tags-ignore: - - v* + branches: [main, develop] + tags-ignore: [v*] pull_request: - branches: - - develop + branches: [develop] jobs: - lints: + lint: name: Lints runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - profile: minimal - toolchain: nightly - override: true components: rustfmt, clippy - - - name: Run cargo check - uses: actions-rs/cargo@v1 - with: - command: check + rustflags: "" - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check + + - name: Run cargo check + run: cargo check --all-targets --all-features - name: Run cargo clippy - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features - name: Clippy Output + run: cargo clippy --all-targets --all-features -- -D warnings build: name: Build - needs: [lints] + needs: [lint] runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - profile: minimal - toolchain: nightly - override: true + rustflags: "" - name: Run cargo build - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features + run: cargo build --all-features - name: Run cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features build-target: - name: Build on ${{ matrix.os }} for ${{ matrix.target }} target - needs: [lints] + name: Build on ${{ matrix.name }} + needs: [lint] runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - target: [''] include: - - os: macos-latest - target: x86_64-apple-darwin - - os: macos-latest - target: aarch64-apple-darwin - - os: macos-latest - target: x86_64-apple-ios - - os: macos-latest - target: aarch64-apple-ios - - os: windows-latest - target: x86_64-pc-windows-gnu - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: windows-latest - target: i686-pc-windows-gnu - os: windows-latest - target: i686-pc-windows-msvc - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - - os: ubuntu-latest - target: i686-unknown-linux-gnu - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: ubuntu-latest - target: aarch64-linux-android - - os: ubuntu-latest - target: armv7-linux-androideabi + name: Windows + - os: macos-latest + name: MacOS steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - profile: minimal - toolchain: nightly - target: ${{ matrix.target }} - override: true + rustflags: "" - name: Run cargo build - uses: actions-rs/cargo@v1 - with: - command: build - args: --all-features + run: cargo build --all-features - name: Run cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features coverage: name: Code coverage @@ -138,30 +82,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - - name: Install nightly toolchain - uses: actions-rs/toolchain@v1 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - profile: minimal - toolchain: nightly - override: true + rustflags: "" - - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.1 - with: - version: 0.22.0 - args: --all-features + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + + - name: Generate coverage report (Cobertura) + run: cargo llvm-cov --workspace --all-features --cobertura --output-path cobertura.xml - name: Upload to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - token: ${{secrets.CODECOV_TOKEN}} - files: ./cobertura.xml + token: ${{ secrets.CODECOV_TOKEN }} + files: cobertura.xml fail_ci_if_error: true - name: Archive code coverage results - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: - name: code-coverage-report + name: coverage-report path: cobertura.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2f1e9..13d2843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.5] - 2026-02-11 +### Changed +- Remove the const generic parameter, making the crate fully compatible with **stable Rust** +(no more nightly toolchain required). +- Introduce a sealed `Case` trait to control casing behavior at the type level. + +### Added +- Add `Uppercase` and `Lowercase` tag types implementing `Case`. + ## [0.1.3] - 2022-04-28 ### Changed - Bump Rust edition to 2021 @@ -28,7 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Convenient type aliases `UpperHexString` and `LowerHexString`. - Feature flag `serde` for serde support on `HexString` type. -[Unreleased]: https://github.com/alekece/hextring-rs/compare/v0.1.3...HEAD +[Unreleased]: https://github.com/alekece/hextring-rs/compare/v0.1.5...HEAD +[0.1.5]: https://github.com/alekece/hexstring-rs/releases/tag/v0.1.5 [0.1.3]: https://github.com/alekece/hexstring-rs/releases/tag/v0.1.3 [0.1.2]: https://github.com/alekece/hexstring-rs/releases/tag/v0.1.2 [0.1.1]: https://github.com/alekece/hexstring-rs/releases/tag/v0.1.1 diff --git a/Cargo.toml b/Cargo.toml index 659e8d5..dd936cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hexstring" -version = "0.1.4" +version = "0.1.5" edition = "2021" license = "MIT" keywords = ["hex", "hexadecimal", "string", "utility"] @@ -14,12 +14,12 @@ rust-version = "1.58.0" maintenance = { status = "actively-developed" } [dependencies] -serde = { version = "1.0.126", default-features = false, features = ["std", "derive"], optional = true} -derive_more = { version = "0.99.16", default-features = false, features = ["display"] } +serde = { version = "1.0.228", default-features = false, features = ["std", "derive"], optional = true} +derive_more = { version = "2.1.1", default-features = false, features = ["display"] } hex = "0.4.3" [dev-dependencies] -serde_json = "1.0.64" +serde_json = "1.0.149" [features] default = ["serde"] diff --git a/README.md b/README.md index 62a1a71..6b3f5f8 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ It allows all the common conversion expected from a hexadecimal string : - Construct from both string and string literal - Convert from and into array of bytes -The [`HexString`](https://docs.rs/hexstring/latest/hexstring/struct.HexString.html) type is a tiny immutable wrapper around string and insure it always contains a -valid hexadecimal string. +The [`HexString`](https://docs.rs/hexstring/latest/hexstring/struct.HexString.html) type is a tiny +immutable wrapper around string and insure it always contains a valid hexadecimal string. ## Feature flags @@ -27,14 +27,6 @@ The following are a list of [Cargo features][cargo-features] that can be enabled -## Requirements -`hexstring` crate uses unstable constant generic type internally. -To compile the library in any project, build it in nightly mode such as : - -``` sh -rustup override set nightly -``` - ## License Licensed under MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 271800c..292fe49 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly" \ No newline at end of file +channel = "stable" diff --git a/src/lib.rs b/src/lib.rs index 41ab681..dddd9a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! - Construct from both string and string literal //! - Convert from and into array of bytes //! -//! The [`HexString`](crate::HexString) type is a tiny immutable wrapper around string and insure it +//! The [`HexString`] type is a tiny immutable wrapper around string and insure it //! always contains a valid hexadecimal string. //! //! ## Feature flags @@ -17,38 +17,80 @@ //! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/features.html#the-features-section //! [serde]: https://serde.rs -#![feature(adt_const_params)] -#![allow(incomplete_features)] #![deny(missing_docs)] use std::borrow::Cow; use std::convert::{From, TryFrom}; -use std::marker::ConstParamTy; -use std::str; -use std::str::FromStr; +use std::marker::PhantomData; +use std::str::{self, FromStr}; use derive_more::Display; use hex::FromHexError; /// Errors than can occurs during [`HexString`] construction. /// -/// Refers to [`FromHexError`][hex::FromHexError] for more details. +/// Refers to [`FromHexError`] for more details. pub type Error = FromHexError; -/// Indicates the case of the hexadecimal string. -#[derive(Debug, PartialEq, Eq, ConstParamTy)] -pub enum Case { - /// Indicates a lowercase hexadecimal string. - Lower, - /// Indicates a uppercase hexadecimal string. - Upper, +/// Convenient alias type to represent uppercase hexadecimal string. +pub type UpperHexString = HexString; + +/// Convenient alias type to represent lowercase hexadecimal string. +pub type LowerHexString = HexString; + +mod private { + /// A sealed trait to prevent external implementations of [`Case`]. + pub trait Sealed {} +} + +/// Provides encoding and validation for hexadecimal strings according to the case. +/// +/// This trait is sealed to prevent external implementations, ensuring that only `Lowercase` and +/// `Uppercase` can be used as cases. +pub trait Case: private::Sealed { + /// Encodes the given bytes into a hexadecimal string according to the case. + fn encode(bytes: &[u8]) -> String; + /// Checks if the given character is a valid hexadecimal character according to the case. + fn is_valid(c: char) -> bool; +} + +/// Lowercase hexadecimal string representation. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Lowercase; + +impl Case for Lowercase { + fn encode(bytes: &[u8]) -> String { + hex::encode(bytes) + } + + fn is_valid(c: char) -> bool { + matches!(c, '0'..='9' | 'a'..='f') + } +} + +impl private::Sealed for Lowercase {} + +/// Uppercase hexadecimal string representation. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Uppercase; + +impl Case for Uppercase { + fn encode(bytes: &[u8]) -> String { + hex::encode_upper(bytes) + } + + fn is_valid(c: char) -> bool { + matches!(c, '0'..='9' | 'A'..='F') + } } +impl private::Sealed for Uppercase {} + /// Provides a structured representation of a hexadecimal string. /// /// It is guaranteed to be a valid hexadecimal string, whether initialized from a string /// or from bytes. -/// A valid ['HexString`] should contain only alphanumerical characters such as : +/// A valid [`HexString`] should contain only alphanumerical characters such as : /// - ff04ad992c /// - FF04AD99C /// @@ -60,13 +102,13 @@ pub enum Case { /// string. /// /// ``` -/// use hexstring::{HexString, Case}; +/// use hexstring::{HexString, Uppercase}; /// -/// let hex = HexString::<{ Case::Upper }>::new("ABCDEF").unwrap(); +/// let hex = HexString::::new("ABCDEF").unwrap(); /// ``` /// /// As the example shown, creating a hexadecimal string is a bit convoluted due to the usage of -/// const generic parameter. +/// generic type. /// Two convenient type aliases must be used instead of the raw [`HexString`] type : /// /// ``` @@ -93,97 +135,86 @@ pub enum Case { serde(try_from = "String") )] #[derive(Clone, Debug, Default, Display, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[display(fmt = "{}", &self.0)] +#[display("{}", _0)] #[repr(transparent)] -pub struct HexString(Cow<'static, str>); - -/// Convenient alias type to represent uppercase hexadecimal string. -pub type UpperHexString = HexString<{ Case::Upper }>; +pub struct HexString(Cow<'static, str>, PhantomData); -/// Convenient alias type to represent lowercase hexadecimal string. -pub type LowerHexString = HexString<{ Case::Lower }>; - -impl HexString { +impl HexString { /// Constructs a new [`HexString`] from a string. /// /// # Errors - /// This method fails if the given string is not a valid hexadecimal. - pub fn new>>(s: S) -> Result { + /// This method fails if the given string is not a valid hexadecimal, i.e. if it has an odd length + /// or contains invalid characters. + pub fn new(s: impl Into>) -> Result { let s = s.into(); if s.len() & 1 != 0 { return Err(Error::OddLength); } - if let Some((index, c)) = s.chars().enumerate().find(|(_, c)| match C { - Case::Lower => !matches!(c, '0'..='9' | 'a'..='f'), - Case::Upper => !matches!(c, '0'..='9' | 'A'..='F'), - }) { + if let Some((index, c)) = s.chars().enumerate().find(|(_, c)| !C::is_valid(*c)) { return Err(Error::InvalidHexCharacter { c, index }); } - Ok(Self(s)) + Ok(Self(s, PhantomData)) } /// Creates a new [`HexString`] without checking the string. /// /// # Safety /// The string should be a valid hexadecimal string. - pub unsafe fn new_unchecked>>(s: S) -> Self { - Self(s.into()) + pub unsafe fn new_unchecked(s: impl Into>) -> Self { + Self(s.into(), PhantomData) } } -impl LowerHexString { - /// Constructs an [`UpperHexString`] from a [`LowerHexString`]. +impl HexString { + /// Constructs an [`HexString`] from a [`HexString`]. /// /// This method performs a copy if the internal string is a string literal. - pub fn to_uppercase(self) -> UpperHexString { + pub fn to_uppercase(self) -> HexString { let mut s = self.0.into_owned(); s.make_ascii_uppercase(); - unsafe { UpperHexString::new_unchecked(s) } + unsafe { HexString::new_unchecked(s) } } } -impl UpperHexString { - /// Constructs a [`LowerHexString`] from an [`UpperHexString`]. +impl HexString { + /// Constructs a [`HexString`] from an [`HexString`]. /// /// This method performs a copy if the internal string is a string literal. - pub fn to_lowercase(self) -> LowerHexString { + pub fn to_lowercase(self) -> HexString { let mut s = self.0.into_owned(); s.make_ascii_lowercase(); - unsafe { LowerHexString::new_unchecked(s) } + unsafe { HexString::new_unchecked(s) } } } -impl From<&[u8]> for HexString { +impl From<&[u8]> for HexString { fn from(bytes: &[u8]) -> Self { - let s = match C { - Case::Upper => hex::encode_upper(bytes), - Case::Lower => hex::encode(bytes), - }; + let s = C::encode(bytes); unsafe { Self::new_unchecked(s) } } } -impl From> for HexString { +impl From> for HexString { fn from(bytes: Vec) -> Self { Self::from(&bytes[..]) } } -impl From<[u8; N]> for HexString { +impl From<[u8; N]> for HexString { fn from(bytes: [u8; N]) -> Self { Self::from(&bytes[..]) } } -impl From> for Vec { +impl From> for Vec { fn from(s: HexString) -> Self { // since `HexString` always represents a valid hexadecimal string, the result of `hex::decode` // can be safely unwrapped. @@ -194,7 +225,7 @@ impl From> for Vec { } } -impl FromStr for HexString { +impl FromStr for HexString { type Err = Error; fn from_str(s: &str) -> Result { @@ -202,7 +233,7 @@ impl FromStr for HexString { } } -impl TryFrom> for [u8; N] { +impl TryFrom> for [u8; N] { type Error = Error; fn try_from(s: HexString) -> Result { @@ -222,7 +253,7 @@ mod seal { use std::convert::TryFrom; #[doc(hidden)] - impl TryFrom for HexString { + impl TryFrom for HexString { type Error = Error; fn try_from(s: String) -> Result { @@ -239,11 +270,11 @@ mod tests { fn it_constructs_from_owned_str() { assert_eq!( LowerHexString::new("ab04ff".to_string()), - Ok(HexString(Cow::Owned("ab04ff".to_string()))) + Ok(HexString(Cow::Owned("ab04ff".to_string()), PhantomData)) ); assert_eq!( UpperHexString::new("AB04FF".to_string()), - Ok(HexString(Cow::Owned("AB04FF".to_string()))) + Ok(HexString(Cow::Owned("AB04FF".to_string()), PhantomData)) ); } @@ -251,11 +282,11 @@ mod tests { fn it_constructs_from_borrowed_str() { assert_eq!( LowerHexString::new("ab04ff"), - Ok(HexString(Cow::Borrowed("ab04ff"))) + Ok(HexString(Cow::Borrowed("ab04ff"), PhantomData)) ); assert_eq!( UpperHexString::new("AB04FF"), - Ok(HexString(Cow::Borrowed("AB04FF"))) + Ok(HexString(Cow::Borrowed("AB04FF"), PhantomData)) ); } @@ -269,19 +300,19 @@ mod tests { fn it_constructs_from_bytes() { assert_eq!( LowerHexString::from([42, 15, 5]), - HexString::<{ Case::Lower }>(Cow::Borrowed("2a0f05")) + HexString::(Cow::Borrowed("2a0f05"), PhantomData) ); assert_eq!( UpperHexString::from([42, 15, 5]), - HexString::<{ Case::Upper }>(Cow::Borrowed("2A0F05")) + HexString::(Cow::Borrowed("2A0F05"), PhantomData) ); assert_eq!( LowerHexString::from(vec![1, 2, 3, 4, 5]), - HexString::<{ Case::Lower }>(Cow::Borrowed("0102030405")) + HexString::(Cow::Borrowed("0102030405"), PhantomData) ); assert_eq!( UpperHexString::from(vec![1, 2, 3, 4, 5]), - HexString::<{ Case::Upper }>(Cow::Borrowed("0102030405")) + HexString::(Cow::Borrowed("0102030405"), PhantomData) ); } @@ -347,11 +378,19 @@ mod tests { assert_eq!(bytes, [20, 42, 2, 10, 15]); } + #[test] + fn it_converts_from_str() { + let hex = "aabbccddee".parse::().unwrap(); + let expected_hex = HexString::(Cow::Owned("aabbccddee".to_string()), PhantomData); + + assert_eq!(hex, expected_hex); + } + #[test] fn it_creates_upper_hex_str_from_lower_hex_str() { let s = "aabbccddee"; let hex = LowerHexString::new(s).unwrap().to_uppercase(); - let expected_hex = HexString::<{ Case::Upper }>(Cow::Owned("AABBCCDDEE".to_string())); + let expected_hex = HexString::(Cow::Owned("AABBCCDDEE".to_string()), PhantomData); assert_ne!(s, hex.0.as_ref()); assert_eq!(hex, expected_hex); @@ -365,7 +404,7 @@ mod tests { fn it_creates_lower_hex_str_from_upper_str() { let s = "AABBCCDDEE"; let hex = UpperHexString::new(s).unwrap().to_lowercase(); - let expected_hex = HexString::<{ Case::Lower }>(Cow::Owned("aabbccddee".to_string())); + let expected_hex = HexString::(Cow::Owned("aabbccddee".to_string()), PhantomData); assert_ne!(s, hex.0.as_ref()); assert_eq!(hex, expected_hex);