diff --git a/.changes/20260708_140000_cardano-api_pablo.lamela_nonthrowing_utf8_decode.yml b/.changes/20260708_140000_cardano-api_pablo.lamela_nonthrowing_utf8_decode.yml new file mode 100644 index 0000000000..ed0f28f5d0 --- /dev/null +++ b/.changes/20260708_140000_cardano-api_pablo.lamela_nonthrowing_utf8_decode.yml @@ -0,0 +1,6 @@ +description: | + Fixed `deserialiseInput`, `deserialiseInputAnyOf` and `deserialiseAnyVerificationKeyBech32` (and thereby `deserialiseAnyVerificationKey`) to return an error instead of throwing an impure exception when the input is not valid UTF-8. Added a `Bech32InvalidUtf8` constructor to `Bech32DecodeError`, which carries the rendered UTF-8 decoding error. +kind: +- bugfix +pr: 1249 +project: cardano-api diff --git a/cardano-api/src/Cardano/Api/Key/Internal/SomeAddressVerificationKey.hs b/cardano-api/src/Cardano/Api/Key/Internal/SomeAddressVerificationKey.hs index 1d7fa9ebef..b6ccbd64eb 100644 --- a/cardano-api/src/Cardano/Api/Key/Internal/SomeAddressVerificationKey.hs +++ b/cardano-api/src/Cardano/Api/Key/Internal/SomeAddressVerificationKey.hs @@ -28,10 +28,12 @@ import Cardano.Api.Serialise.TextEnvelope.Internal import Cardano.Chain.Common qualified as Common import Cardano.Crypto.Signing qualified as Crypto +import Control.Exception (displayException) import Data.Aeson qualified as Aeson import Data.Bifunctor (first) import Data.ByteString (ByteString) import Data.Text (Text) +import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Formatting (build, sformat, (%)) @@ -142,8 +144,11 @@ deserialiseAnyVerificationKey bs = deserialiseAnyVerificationKeyBech32 :: ByteString -> Either Bech32DecodeError SomeAddressVerificationKey -deserialiseAnyVerificationKeyBech32 = - deserialiseAnyOfFromBech32 allBech32VerKey . Text.decodeUtf8 +deserialiseAnyVerificationKeyBech32 bs = + case Text.decodeUtf8' bs of + -- The input is not valid UTF-8, so it cannot be valid Bech32. + Left err -> Left $ Bech32InvalidUtf8 $ Text.pack $ displayException err + Right text -> deserialiseAnyOfFromBech32 allBech32VerKey text where allBech32VerKey :: [FromSomeType SerialiseAsBech32 SomeAddressVerificationKey] diff --git a/cardano-api/src/Cardano/Api/Serialise/Bech32.hs b/cardano-api/src/Cardano/Api/Serialise/Bech32.hs index 6fa1840434..c4633bb847 100644 --- a/cardano-api/src/Cardano/Api/Serialise/Bech32.hs +++ b/cardano-api/src/Cardano/Api/Serialise/Bech32.hs @@ -149,6 +149,9 @@ data Bech32DecodeError -- ^ Expected header !Text -- ^ Unexpected header + | -- | The input is not valid UTF-8, so it cannot be a Bech32-encoded + -- string. The field contains the rendered UTF-8 decoding error. + Bech32InvalidUtf8 !Text deriving (Eq, Show, Data) instance Error Bech32DecodeError where @@ -181,3 +184,5 @@ instance Error Bech32DecodeError where [ "Unexpected CIP-129 Bech32 header: the actual header is " <> pshow actual , ", but it was expected to be " <> pshow expected ] + Bech32InvalidUtf8 decodeErr -> + "The Bech32-encoded string is not valid UTF-8: " <> pretty decodeErr diff --git a/cardano-api/src/Cardano/Api/Serialise/DeserialiseAnyOf.hs b/cardano-api/src/Cardano/Api/Serialise/DeserialiseAnyOf.hs index 6fce35def4..abbc738907 100644 --- a/cardano-api/src/Cardano/Api/Serialise/DeserialiseAnyOf.hs +++ b/cardano-api/src/Cardano/Api/Serialise/DeserialiseAnyOf.hs @@ -35,7 +35,6 @@ import Data.ByteString.Char8 qualified as BSC import Data.Char (toLower) import Data.Data import Data.List.NonEmpty (NonEmpty) -import Data.Text (Text) import Data.Text.Encoding qualified as Text import GHC.Exts (IsList (..)) import Prettyprinter @@ -107,9 +106,6 @@ deserialiseInput deserialiseInput acceptedFormats inputBs = go (toList acceptedFormats) where - inputText :: Text - inputText = Text.decodeUtf8 inputBs - go :: [InputFormat a] -> Either InputDecodeError a go [] = Left InputInvalidError go (kf : kfs) = @@ -139,13 +135,19 @@ deserialiseInput acceptedFormats inputBs = Left _ -> DeserialiseInputErrorFormatMismatch deserialiseBech32 :: SerialiseAsBech32 a => DeserialiseInputResult a - deserialiseBech32 = - case deserialiseFromBech32 inputText of - Right res -> DeserialiseInputSuccess res - -- The input was not valid Bech32. - Left (Bech32DecodingError _) -> DeserialiseInputErrorFormatMismatch - -- The input was valid Bech32, but some other error occurred. - Left err -> DeserialiseInputError $ InputBech32DecodeError err + deserialiseBech32 = either id fromBech32 $ decodeText inputBs + where + -- The input was not valid UTF-8, so it cannot be valid Bech32. + decodeText = + first (const DeserialiseInputErrorFormatMismatch) + . Text.decodeUtf8' + fromBech32 text = + case deserialiseFromBech32 text of + Right res -> DeserialiseInputSuccess res + -- The input was not valid Bech32. + Left (Bech32DecodingError _) -> DeserialiseInputErrorFormatMismatch + -- The input was valid Bech32, but some other error occurred. + Left err -> DeserialiseInputError $ InputBech32DecodeError err deserialiseHex :: SerialiseAsRawBytes a => DeserialiseInputResult a deserialiseHex @@ -179,9 +181,6 @@ deserialiseInputAnyOf bech32Types textEnvTypes inputBs = DeserialiseInputError err -> Left err DeserialiseInputErrorFormatMismatch -> Left InputInvalidError where - inputText :: Text - inputText = Text.decodeUtf8 inputBs - orTry :: DeserialiseInputResult b -> DeserialiseInputResult b @@ -208,13 +207,19 @@ deserialiseInputAnyOf bech32Types textEnvTypes inputBs = Left _ -> DeserialiseInputErrorFormatMismatch deserialiseBech32 :: DeserialiseInputResult b - deserialiseBech32 = - case deserialiseAnyOfFromBech32 bech32Types inputText of - Right res -> DeserialiseInputSuccess res - -- The input was not valid Bech32. - Left (Bech32DecodingError _) -> DeserialiseInputErrorFormatMismatch - -- The input was valid Bech32, but some other error occurred. - Left err -> DeserialiseInputError $ InputBech32DecodeError err + deserialiseBech32 = either id fromBech32 $ decodeText inputBs + where + -- The input was not valid UTF-8, so it cannot be valid Bech32. + decodeText = + first (const DeserialiseInputErrorFormatMismatch) + . Text.decodeUtf8' + fromBech32 text = + case deserialiseAnyOfFromBech32 bech32Types text of + Right res -> DeserialiseInputSuccess res + -- The input was not valid Bech32. + Left (Bech32DecodingError _) -> DeserialiseInputErrorFormatMismatch + -- The input was valid Bech32, but some other error occurred. + Left err -> DeserialiseInputError $ InputBech32DecodeError err -- | Read formatted file readFormattedFile diff --git a/cardano-api/test/cardano-api-golden/Test/Golden/ErrorsSpec.hs b/cardano-api/test/cardano-api-golden/Test/Golden/ErrorsSpec.hs index 08c26077a7..5a63ce34a4 100644 --- a/cardano-api/test/cardano-api-golden/Test/Golden/ErrorsSpec.hs +++ b/cardano-api/test/cardano-api-golden/Test/Golden/ErrorsSpec.hs @@ -159,6 +159,7 @@ test_Bech32DecodeError = , Bech32DeserialiseFromBytesError bytestring , Bech32WrongPrefix text text , Bech32UnexpectedHeader text text + , Bech32InvalidUtf8 text ] test_InputDecodeError :: TestTree diff --git a/cardano-api/test/cardano-api-golden/files/errors/Cardano.Api.Serialise.Bech32.Bech32DecodeError/Bech32InvalidUtf8.txt b/cardano-api/test/cardano-api-golden/files/errors/Cardano.Api.Serialise.Bech32.Bech32DecodeError/Bech32InvalidUtf8.txt new file mode 100644 index 0000000000..5338dd2070 --- /dev/null +++ b/cardano-api/test/cardano-api-golden/files/errors/Cardano.Api.Serialise.Bech32.Bech32DecodeError/Bech32InvalidUtf8.txt @@ -0,0 +1 @@ +The Bech32-encoded string is not valid UTF-8: \ No newline at end of file