From 4856ec516f0733c8aef065166a417a358f12fe62 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 15:18:01 -0600 Subject: [PATCH 01/11] Borsh serialization for HexString --- CHANGELOG.md | 1 + README.md | 5 ---- hexstring.cabal | 3 ++ package.yaml | 3 ++ src/Data/HexString.hs | 64 +++++++++++++++++++++++++++---------------- stack.yaml | 6 ++++ 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e74184..67385b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CHANGELOG.md - Stack integration +- Borsh serialization for `HexString` ### Changed diff --git a/README.md b/README.md index 45afa52..5bdd5d1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ haskell-hexstring ================= -[![Build Status](https://travis-ci.org/solatis/haskell-hexstring.png?branch=master)](https://travis-ci.org/solatis/haskell-hexstring) -[![Coverage Status](https://coveralls.io/repos/solatis/haskell-hexstring/badge.svg?branch=master)](https://coveralls.io/r/solatis/haskell-hexstring?branch=master) -[![MIT](http://b.repl.ca/v1/license-MIT-blue.png)](http://en.wikipedia.org/wiki/MIT_License) -[![Haskell](http://b.repl.ca/v1/language-haskell-lightgrey.png)](http://haskell.org) - Fast and safe representation of a hex string diff --git a/hexstring.cabal b/hexstring.cabal index 56b5d84..a675077 100644 --- a/hexstring.cabal +++ b/hexstring.cabal @@ -35,7 +35,10 @@ library , base >=4.7 && <5 , base16-bytestring , binary + , borsh >=0.2 , bytestring + , foreign-rust + , generics-sop , text default-language: Haskell2010 diff --git a/package.yaml b/package.yaml index 680f2d2..bee7374 100644 --- a/package.yaml +++ b/package.yaml @@ -27,6 +27,9 @@ library: - bytestring - base16-bytestring - aeson + - generics-sop + - borsh >= 0.2 + - foreign-rust tests: hextring-test: diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 3caebcf..d66ae6c 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -1,36 +1,53 @@ -module Data.HexString ( HexString - , hexString - , fromBinary - , toBinary - , fromBytes - , toBytes - , toText ) where +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE OverloadedStrings #-} -import Control.Applicative (pure) +module Data.HexString + ( HexString + , hexString + , fromBinary + , toBinary + , fromBytes + , toBytes + , toText + ) where -import Data.Aeson -import Data.Word (Word8) +import Codec.Borsh +import Control.Applicative (pure) -import qualified Data.ByteString as BS +import Data.Aeson +import Data.Word (Word8) + +import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as BS16 (decodeLenient, encode) -import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy as BSL -import qualified Data.Text as T -import qualified Data.Text.Encoding as TE +import Data.Structured +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE -import qualified Data.Binary as B (Binary, decode, encode) +import qualified Data.Binary as B (Binary, decode, encode) + +import qualified GHC.Generics as GHC +import qualified Generics.SOP as SOP -- | Represents a Hex string. Guarantees that all characters it contains -- are valid hex characters. data HexString = HexString BS.ByteString - deriving ( Show, Eq, Ord ) + deriving stock (Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct HexString instance FromJSON HexString where parseJSON = withText "HexString" $ pure . hexString . TE.encodeUtf8 instance ToJSON HexString where - toJSON = String . toText + toJSON = Data.Aeson.String . toText -- | Smart constructor which validates that all the text are actually -- hexadecimal characters. @@ -38,16 +55,15 @@ hexString :: BS.ByteString -> HexString hexString bs = let isValidHex :: Word8 -> Bool isValidHex c - | (48 <= c) && (c < 58) = True + | (48 <= c) && (c < 58) = True | (97 <= c) && (c < 103) = True - | otherwise = False - - in if BS.all isValidHex bs - then HexString bs - else error ("Not a valid hex string: " ++ show bs) + | otherwise = False + in if BS.all isValidHex bs + then HexString bs + else error ("Not a valid hex string: " ++ Prelude.show bs) -- | Converts a 'B.Binary' to a 'HexString' value -fromBinary :: B.Binary a => a -> HexString +fromBinary :: B.Binary a => a -> HexString fromBinary = hexString . BS16.encode . BSL.toStrict . B.encode -- | Converts a 'HexString' to a 'B.Binary' value diff --git a/stack.yaml b/stack.yaml index 346aa08..7879a40 100644 --- a/stack.yaml +++ b/stack.yaml @@ -2,3 +2,9 @@ resolver: lts-21.22 packages: - . + +extra-deps: + - git: https://github.com/well-typed/borsh.git + commit: d2fcfa159e0a844b1ec5e8ed3e232d4b380fa831 + - git: https://git.vergara.tech/Vergara_Tech/haskell-foreign-rust.git + commit: 787c2e813eb3a5d16c375d4b37dfefbd2adcdf05 From 268eb1cd53b4401f7073ded000768fe52efa392a Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 15:21:16 -0600 Subject: [PATCH 02/11] Add Eq instance --- src/Data/HexString.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index d66ae6c..f8d8d24 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -38,7 +38,7 @@ import qualified Generics.SOP as SOP -- are valid hex characters. data HexString = HexString BS.ByteString - deriving stock (Prelude.Show, GHC.Generic) + deriving stock (Eq, Prelude.Show, GHC.Generic) deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct HexString From a7adee13de4c8f6024ff101b8f1eab30c4dc5e2a Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 15:31:19 -0600 Subject: [PATCH 03/11] Expose HexString constructor --- src/Data/HexString.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index f8d8d24..f1d1968 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -6,7 +6,7 @@ {-# LANGUAGE OverloadedStrings #-} module Data.HexString - ( HexString + ( HexString(..) , hexString , fromBinary , toBinary From be830be7146691557d1a0559d51eb882f77578d0 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 15:31:19 -0600 Subject: [PATCH 04/11] Expose HexString constructor --- src/Data/HexString.hs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index f8d8d24..2fa7d1b 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -6,7 +6,7 @@ {-# LANGUAGE OverloadedStrings #-} module Data.HexString - ( HexString + ( HexString(..) , hexString , fromBinary , toBinary @@ -36,12 +36,12 @@ import qualified Generics.SOP as SOP -- | Represents a Hex string. Guarantees that all characters it contains -- are valid hex characters. -data HexString = - HexString BS.ByteString - deriving stock (Eq, Prelude.Show, GHC.Generic) - deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) - deriving anyclass (Data.Structured.Show) - deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct HexString +newtype HexString = HexString + { bytes :: BS.ByteString + } deriving stock (Eq, Prelude.Show, GHC.Generic) + deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) + deriving anyclass (Data.Structured.Show) + deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct HexString instance FromJSON HexString where parseJSON = withText "HexString" $ pure . hexString . TE.encodeUtf8 From 8c634d8aa6dcbbb718835e8dda39a8c776d2a2dd Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 20:25:06 -0600 Subject: [PATCH 05/11] Update accesor for HexString --- src/Data/HexString.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 2fa7d1b..273a70a 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -37,7 +37,7 @@ import qualified Generics.SOP as SOP -- | Represents a Hex string. Guarantees that all characters it contains -- are valid hex characters. newtype HexString = HexString - { bytes :: BS.ByteString + { hexBytes :: BS.ByteString } deriving stock (Eq, Prelude.Show, GHC.Generic) deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) From fd70a681dc6df30bbd7c019931feff8c024bdbd3 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 20:42:22 -0600 Subject: [PATCH 06/11] Add `fromText` function --- src/Data/HexString.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 273a70a..f0b5d79 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -80,6 +80,10 @@ fromBytes = hexString . BS16.encode toBytes :: HexString -> BS.ByteString toBytes (HexString bs) = BS16.decodeLenient bs +-- | Reads a human-readable hex string into a `HexString` +fromText :: T.Text -> HexString +fromText = hexString . TE.encodeUtf8 + -- | Access to a 'T.Text' representation of the 'HexString' toText :: HexString -> T.Text toText (HexString bs) = TE.decodeUtf8 bs From 49bceb5f14831eaab9c644072cc0edd03c305ea0 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Mon, 5 Feb 2024 20:46:05 -0600 Subject: [PATCH 07/11] Expose `fromText` --- src/Data/HexString.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index f0b5d79..ef71b4b 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -12,6 +12,7 @@ module Data.HexString , toBinary , fromBytes , toBytes + , fromText , toText ) where From 309dd06a2ecae03aaa7c4ed429884caec85273a7 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 6 Feb 2024 11:47:39 -0600 Subject: [PATCH 08/11] Correct `fromText` --- src/Data/HexString.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index ef71b4b..79e3682 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -83,7 +83,7 @@ toBytes (HexString bs) = BS16.decodeLenient bs -- | Reads a human-readable hex string into a `HexString` fromText :: T.Text -> HexString -fromText = hexString . TE.encodeUtf8 +fromText = hexString . BS16.decodeLenient . TE.encodeUtf8 -- | Access to a 'T.Text' representation of the 'HexString' toText :: HexString -> T.Text From fe2df6f7d63272ac147911c1573550bed1d38a37 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 6 Feb 2024 13:05:38 -0600 Subject: [PATCH 09/11] Implement tests --- src/Data/HexString.hs | 36 ++++++++++++++++-------------------- test/Data/HexStringSpec.hs | 28 ++++++++++++++-------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 79e3682..467552f 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -8,9 +8,7 @@ module Data.HexString ( HexString(..) , hexString - , fromBinary - , toBinary - , fromBytes + , fromRawBytes , toBytes , fromText , toText @@ -23,7 +21,7 @@ import Data.Aeson import Data.Word (Word8) import qualified Data.ByteString as BS -import qualified Data.ByteString.Base16 as BS16 (decodeLenient, encode) +import qualified Data.ByteString.Base16 as BS16 (decode, decodeLenient, encode) import qualified Data.ByteString.Lazy as BSL import Data.Structured @@ -39,11 +37,14 @@ import qualified Generics.SOP as SOP -- are valid hex characters. newtype HexString = HexString { hexBytes :: BS.ByteString - } deriving stock (Eq, Prelude.Show, GHC.Generic) + } deriving stock (Eq, GHC.Generic) deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo) deriving anyclass (Data.Structured.Show) deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct HexString +instance Prelude.Show HexString where + show = T.unpack . toText + instance FromJSON HexString where parseJSON = withText "HexString" $ pure . hexString . TE.encodeUtf8 @@ -54,37 +55,32 @@ instance ToJSON HexString where -- hexadecimal characters. hexString :: BS.ByteString -> HexString hexString bs = - let isValidHex :: Word8 -> Bool - isValidHex c - | (48 <= c) && (c < 58) = True - | (97 <= c) && (c < 103) = True - | otherwise = False - in if BS.all isValidHex bs - then HexString bs - else error ("Not a valid hex string: " ++ Prelude.show bs) + case BS16.decode bs of + Right s -> HexString s + Left e -> error e -- | Converts a 'B.Binary' to a 'HexString' value fromBinary :: B.Binary a => a -> HexString -fromBinary = hexString . BS16.encode . BSL.toStrict . B.encode +fromBinary = HexString . BSL.toStrict . B.encode -- | Converts a 'HexString' to a 'B.Binary' value toBinary :: B.Binary a => HexString -> a -toBinary (HexString bs) = B.decode . BSL.fromStrict . BS16.decodeLenient $ bs +toBinary (HexString bs) = (B.decode . BSL.fromStrict) bs -- | Reads a 'BS.ByteString' as raw bytes and converts to hex representation. We -- cannot use the instance Binary of 'BS.ByteString' because it provides -- a leading length, which is not what we want when dealing with raw bytes. -fromBytes :: BS.ByteString -> HexString -fromBytes = hexString . BS16.encode +fromRawBytes :: BS.ByteString -> HexString +fromRawBytes = HexString -- | Access to the raw bytes in a 'BS.ByteString' format. toBytes :: HexString -> BS.ByteString -toBytes (HexString bs) = BS16.decodeLenient bs +toBytes (HexString bs) = bs -- | Reads a human-readable hex string into a `HexString` fromText :: T.Text -> HexString -fromText = hexString . BS16.decodeLenient . TE.encodeUtf8 +fromText = hexString . TE.encodeUtf8 -- | Access to a 'T.Text' representation of the 'HexString' toText :: HexString -> T.Text -toText (HexString bs) = TE.decodeUtf8 bs +toText (HexString bs) = (TE.decodeUtf8 . BS16.encode) bs diff --git a/test/Data/HexStringSpec.hs b/test/Data/HexStringSpec.hs index 93b4b90..a8d6673 100644 --- a/test/Data/HexStringSpec.hs +++ b/test/Data/HexStringSpec.hs @@ -1,27 +1,27 @@ +{-# LANGUAGE OverloadedStrings #-} + module Data.HexStringSpec where -import Data.HexString ( hexString - , fromBytes - , toBytes ) +import Data.HexString (fromRawBytes, fromText, hexString, toBytes, toText) import qualified Data.ByteString.Char8 as BS8 -import Test.Hspec +import Test.Hspec spec :: Spec spec = do describe "when constructing a hex string" $ do - it "should accept strings that fall within a valid range" $ - hexString (BS8.pack "0123456789abcdef") `shouldBe` hexString (BS8.pack "0123456789abcdef") - it "should reject strings outside the range" $ do - putStrLn (show (hexString (BS8.pack "/"))) `shouldThrow` anyErrorCall - putStrLn (show (hexString (BS8.pack ":"))) `shouldThrow` anyErrorCall - putStrLn (show (hexString (BS8.pack "`"))) `shouldThrow` anyErrorCall - putStrLn (show (hexString (BS8.pack "g"))) `shouldThrow` anyErrorCall - + print (hexString (BS8.pack "/")) `shouldThrow` anyErrorCall + print (hexString (BS8.pack ":")) `shouldThrow` anyErrorCall + print (hexString (BS8.pack "`")) `shouldThrow` anyErrorCall + print (hexString (BS8.pack "g")) `shouldThrow` anyErrorCall describe "when interpreting a hex string" $ do it "should convert the hex string properly when interpreting as bytes" $ - toBytes (hexString (BS8.pack "ffff")) `shouldBe` BS8.pack "\255\255" + toBytes (hexString "ffff") `shouldBe` BS8.pack "\255\255" it "should convert bytes to the proper hex string" $ - fromBytes (BS8.pack "\255\255") `shouldBe` hexString (BS8.pack "ffff") + fromRawBytes (BS8.pack "\255\255") `shouldBe` hexString (BS8.pack "ffff") + it "should convert the hex string to text" $ + toText (hexString "ffff") `shouldBe` "ffff" + it "should read text into the hex string" $ + fromText "ffff" `shouldBe` hexString (BS8.pack "ffff") From fd1ddce73c0ad18a2a4509a299c6e93f8c6c383d Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Fri, 9 Feb 2024 13:18:02 -0600 Subject: [PATCH 10/11] Implement `Read` --- CHANGELOG.md | 1 + src/Data/HexString.hs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67385b0..86fae43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CHANGELOG.md - Stack integration - Borsh serialization for `HexString` +- `Read` implementation for `HexString` ### Changed diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 467552f..7d90d24 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -32,6 +32,7 @@ import qualified Data.Binary as B (Binary, decode, encode) import qualified GHC.Generics as GHC import qualified Generics.SOP as SOP +import Text.Read -- | Represents a Hex string. Guarantees that all characters it contains -- are valid hex characters. @@ -45,6 +46,9 @@ newtype HexString = HexString instance Prelude.Show HexString where show = T.unpack . toText +instance Read HexString where + readsPrec _ s = [((fromText . T.pack) s, "")] + instance FromJSON HexString where parseJSON = withText "HexString" $ pure . hexString . TE.encodeUtf8 From 650febad655e7f0f4341a1834701f3987fcdbb97 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 12 Mar 2024 13:52:29 -0500 Subject: [PATCH 11/11] Expose `fromBinary` --- hexstring.cabal | 2 +- package.yaml | 2 +- src/Data/HexString.hs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hexstring.cabal b/hexstring.cabal index a675077..23eb6cd 100644 --- a/hexstring.cabal +++ b/hexstring.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: hexstring -version: 0.12.0 +version: 0.12.1.0 synopsis: Fast and safe representation of a hex string description: Provides an interface for converting any object that has a 'Binary' instance to and from a hexadecimal Text representation. author: Rene Vergara, diff --git a/package.yaml b/package.yaml index bee7374..a1a042d 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: hexstring -version: 0.12.0 +version: 0.12.1.0 git: "https://git.vergara.tech/Vergara_Tech/haskell-hexstring" license: MIT author: diff --git a/src/Data/HexString.hs b/src/Data/HexString.hs index 7d90d24..10a5200 100644 --- a/src/Data/HexString.hs +++ b/src/Data/HexString.hs @@ -9,6 +9,7 @@ module Data.HexString ( HexString(..) , hexString , fromRawBytes + , fromBinary , toBytes , fromText , toText