packages feed

cardano-addresses-4.0.0: lib/Codec/Binary/Encoding.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}

{-# OPTIONS_HADDOCK hide #-}

-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0

module Codec.Binary.Encoding
    ( -- * Types
      AbstractEncoding (..)
    , Encoding

      -- * Encode
    , encode

      -- * Decode
    , detectEncoding
    , fromBase16
    , fromBase64
    , fromBase58
    , fromBech32
    ) where

import Prelude

import Codec.Binary.Bech32
    ( HumanReadablePart )
import Control.Applicative
    ( (<|>) )
import Control.Arrow
    ( left )
import Control.Monad
    ( guard )
import Data.ByteArray.Encoding
    ( Base (..), convertFromBase, convertToBase )
import Data.ByteString
    ( ByteString )
import Data.ByteString.Base58
    ( bitcoinAlphabet, decodeBase58, encodeBase58, unAlphabet )
import Data.Char
    ( isLetter, isLower, isUpper, ord, toLower )

import qualified Codec.Binary.Bech32 as Bech32
import qualified Data.Text as T
import qualified Data.Text.Encoding as T


--
-- Encoding
--

-- | A concrete 'Encoding' algebraic data-type.
type Encoding = AbstractEncoding HumanReadablePart

-- | An abstract 'Encoding' to make it easy to map over the bech32 component.
-- Typically used as 'AbstractEncoding HumanReadablePart'.
--
-- > λ> let xpubHRP = [humanReadablePart|xpub|]
-- > λ> let xprvHRP = [humanReadablePart|xprv|]
-- >
-- > λ> fmap (const xpubHRP) (EBech32 xprvHRP)
-- > EBech32 (HumanReadablePart "xpub")
--
data AbstractEncoding a
    = EBase16
    | EBase58
    | EBech32 a
    deriving (Eq, Show, Functor)

--
-- Encode
--


-- | Encode a 'ByteString' with the given encoding.
--
-- @since 2.0.0
encode :: Encoding -> ByteString -> ByteString
encode encoding bytes = case encoding of
    EBase16 ->
        convertToBase Base16 bytes
    EBase58 ->
        encodeBase58 bitcoinAlphabet bytes
    EBech32 hrp ->
        T.encodeUtf8 $ Bech32.encodeLenient hrp $ Bech32.dataPartFromBytes bytes

--
-- Decode
--

-- | Try detecting the encoding of a given 'String'
--
-- @since 2.0.0
detectEncoding :: String -> Maybe (AbstractEncoding ())
detectEncoding str = isBase16 <|> isBech32  <|> isBase58
  where
    isBase16 = do
        guard (all ((`elem` "0123456789abcdef") . toLower) str)
        guard (even (length str))
        pure EBase16

    isBech32 = do
        guard (not (null humanpart))
        guard (all (\c -> ord c >= 33 && ord c <= 126) humanpart)
        guard (length datapart >= 6)
        guard (all (`elem` Bech32.dataCharList) datapart)
        guard (all isUpper alpha || all isLower alpha)
        pure (EBech32 ())
      where
        datapart  = reverse . takeWhile (/= '1') . reverse $ str
        humanpart = takeWhile (/= '1') str
        alpha = filter isLetter str

    isBase58 = do
        guard (all (`elem` T.unpack (T.decodeUtf8 $ unAlphabet bitcoinAlphabet)) str)
        pure EBase58

-- | Try decoding a base16-encoded 'ByteString'
--
-- @since 2.0.0
fromBase16 :: ByteString -> Either String ByteString
fromBase16 = convertFromBase Base16

-- | Try decoding a base64-encoded 'ByteString'
--
-- @since 3.13.0
fromBase64 :: ByteString -> Either String ByteString
fromBase64 = convertFromBase Base64

-- | Try decoding a bech32-encoded 'ByteString'
--
-- @since 2.0.0
fromBech32
    :: ([Int] -> String -> String)
    -> ByteString
    -> Either String (HumanReadablePart, ByteString)
fromBech32 markCharsRedAtIndices raw = left errToString $ do
    (hrp, dp) <- left Just $ Bech32.decodeLenient $ T.decodeUtf8 raw
    maybe (Left Nothing) (Right . (hrp,)) $ Bech32.dataPartToBytes dp
  where
    unCharPos (Bech32.CharPosition x) = x
    invalidCharsMsg = "Invalid character(s) in string"
    errToString = ("Bech32 error: " <>) . \case
        Just Bech32.StringToDecodeTooLong ->
            "string is too long"
        Just Bech32.StringToDecodeTooShort ->
            "string is too short"
        Just Bech32.StringToDecodeHasMixedCase ->
            "string has mixed case"
        Just Bech32.StringToDecodeMissingSeparatorChar ->
            "string has no separator char"
        Just (Bech32.StringToDecodeContainsInvalidChars []) ->
            invalidCharsMsg
        Just (Bech32.StringToDecodeContainsInvalidChars ixs) ->
            invalidCharsMsg <> ":\n" <> markCharsRedAtIndices
                (map unCharPos ixs)
                (T.unpack . T.decodeUtf8 $ raw)
        Nothing ->
            "invalid data-part; these bytes ain't uint8."

fromBase58 :: ByteString -> Either String ByteString
fromBase58 raw = maybe (Left "Invalid Base58-encoded string.") Right $ do
    decodeBase58 bitcoinAlphabet raw