packages feed

fast-bech32-1.0.0: src/Data/ByteString/Bech32.hs

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

-- | This package provides an implementation of the bech32 encoding. In essence,
-- this encoding is comprised of three parts:
--
-- - A human readable prefix
-- - A base32-encoded payload (with a modified alphabet to avoid similar letters)
-- - A checksum calculated on the prefix and the encoded payload
--
-- The implementation is optimized on mainly three aspects:
--
-- - Low-level memory manipulation with typed pointers is used to traverse the
--   input string and to write the encoded value. Each byte is encoded
--   on-the-fly, which requires keeping track of a residue (since we encode from
--   8 bit to 5 bit, but can only read 8 bit at the same, the remainder is
--   stored and passed to the next builder loop).
--
-- - Since the checksum is calculated by folding over the encoded bytestring, it
--   can be computed in-place, as the string is being encoded. This is exactly
--   what this implementation does as well. In such way that once the string is
--   encoded, the checksum is already calculated and only need to be base32
--   encoded in turn.
--
-- - The first part of the checksum is pre-computed during the construction of the
--   HRP. It is indeed quite common for a single HRP to encode many payload. To
--   calculate the checksum, one must first expand the HRP which is in itself an
--   already 'costly' operation. Pre-calculating the expansion and the beginning
--   of the checksum shaves off some time .
--
module Data.ByteString.Bech32
    ( -- * Encoding
      encodeBech32

      -- * HumanReadablePart
    , HumanReadablePart(HumanReadablePart)
    , unHumanReadablePart
    ) where

import Relude

import Data.Bits
    ( Bits, testBit, unsafeShiftL, unsafeShiftR, (.&.), (.|.) )
import Data.ByteString.Internal
    ( ByteString (..) )
import Foreign.ForeignPtr
    ( withForeignPtr )
import Foreign.Ptr
    ( Ptr, plusPtr )
import Foreign.Storable
    ( peek, poke )
import GHC.Exts
    ( Addr#, indexWord8OffAddr#, word2Int# )
import GHC.ForeignPtr
    ( mallocPlainForeignPtrBytes )
import GHC.Word
    ( Word8 (..) )
import System.IO.Unsafe
    ( unsafeDupablePerformIO )

import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Data.Text.Encoding as T

-- | Encode some binary data to bech32 using the given human readable prefix.
encodeBech32 :: HumanReadablePart -> ByteString -> Text
encodeBech32 hrp bytes = mconcat
    [ prefix hrp
    , "1"
    , encodeDataPart "qpzry9x8gf2tvdw0s3jn54khce6mua7l"# (checksum hrp) bytes
    ]

--
-- HumanReadablePart
--

data HumanReadablePart = HumanReadablePartConstr
    { prefix :: !Text
    , checksum :: !Checksum
    } deriving Show

-- | Construct a human readable part from a text string, and pre-calculate the
-- checksum corresponding to it.
pattern HumanReadablePart :: Text -> HumanReadablePart
pattern HumanReadablePart { unHumanReadablePart } <-
    HumanReadablePartConstr unHumanReadablePart _
  where
    HumanReadablePart prefix =
        let
            checksum =
                foldl' (flip polymodStep) (Checksum 1) $ expand (T.unpack prefix)

            expand :: [Char] -> [Word5]
            expand xs =
                [ coerce @Word8 (fromIntegral (ord x) .>>. 5) | x <- xs ]
                ++
                [ coerce @Word8 0 ]
                ++
                [ coerce @Word8 (fromIntegral (ord x) .&. 31) | x <- xs ]
        in
            HumanReadablePartConstr {prefix,checksum}
{-# COMPLETE HumanReadablePart #-}

--
-- Main encoding loop
--

encodeDataPart :: Addr# -> Checksum -> ByteString -> Text
encodeDataPart !alphabet !chk0 =
    T.decodeUtf8 . withAllocatedPointers (base32 0 (Residue 0) chk0)
  where
    withAllocatedPointers
        :: (Int -> Ptr Word8 -> Ptr Word8 -> IO Checksum)
        -> ByteString
        -> ByteString
    withAllocatedPointers _fn "" = ""
    withAllocatedPointers fn (PS !inputForeignPtr !_ !inputLen) =
        let (!q, !r) = (inputLen * 8) `quotRem` 5 in
        let !resultLen = q + if r == 0 then 0 else 1 in
        unsafeDupablePerformIO $ do
            resultForeignPtr <- mallocPlainForeignPtrBytes resultLen
            withForeignPtr resultForeignPtr $ \resultPtr ->
                withForeignPtr inputForeignPtr $ \inputPtr -> do
                    chk' <- fn (resultLen - 1) inputPtr resultPtr
                    return $ (PS resultForeignPtr 0 resultLen)
                          <> (encodeChecksum alphabet chk')

    base32 :: Int -> Residue -> Checksum -> Int -> Ptr Word8 -> Ptr Word8 -> IO Checksum
    base32 !n !r !chk !maxN !inputPtr !resultPtr
        | n >= maxN = do
            let !w = coerce r
            poke resultPtr (alphabet `lookupWord5` w)
            return $ polymodStep w chk
        | otherwise = do
            (w, r', inputPtr') <- peekWord5 n r inputPtr
            poke resultPtr (alphabet `lookupWord5` w)
            let !chk' = polymodStep w chk
            base32 (n+1) r' chk' maxN inputPtr' (plusPtr resultPtr 1)

--
-- Checksum
--

newtype Checksum = Checksum Word64 deriving Show

encodeChecksum :: Addr# -> Checksum -> ByteString
encodeChecksum !alphabet !chk =
    [ alphabet `lookupWord5` word5 (polymod .>>. i)
    | i <- [25, 20 .. 0 ]
    ] & BS.pack
  where
    polymod =
        let z = coerce @Word8 0
        in (coerce (foldl' (flip polymodStep) chk [z,z,z,z,z,z]) .&. 0x3fffffff) `xor` 1

    word5 :: Word64 -> Word5
    word5 = coerce . fromIntegral @Word64 @Word8 . (.&. 31)
    {-# INLINE word5 #-}

polymodStep :: Word5 -> Checksum -> Checksum
polymodStep (Word5 !v) (Checksum !chk) =
    let chk' = (chk .<<. 5) `xor` fromIntegral v in
    Checksum
        ( xor (if testBit chk 29 then 0x2a1462b3 else 0)
            ( xor (if testBit chk 28 then 0x3d4233dd else 0)
                ( xor (if testBit chk 27 then 0x1ea119fa else 0)
                    ( xor (if testBit chk 26 then 0x26508e6d else 0)
                        ( xor (if testBit chk 25 then 0x3b6a57b2 else 0)
                            chk'
                        )
                    )
                )
            )
        )

--
-- Base32
--

newtype Word5 = Word5 Word8 deriving Show

newtype Residue = Residue Word8 deriving Show

-- | Lookup a Word5 using the given pointer and a previous 'Residue'. Returns
-- the looked up 'Word5', a 'Residue' and the pointer advanced to the next
-- word;
--
-- NOTE: @n = i .&. 7@ is a fast modulo equivalent to @n = i `mod` 8@
peekWord5 :: Int -> Residue -> Ptr Word8 -> IO (Word5, Residue, Ptr Word8)
peekWord5 !((.&. 7) -> !n) (Residue !r) !ptr
    | n == 0 = do
        w <- peek ptr
        return
            ( coerce (w .>>. 3)
            , coerce ((w .&. 0b00000111) .<<. 2)
            , plusPtr ptr 1
            )
    | n == 1 = do
        w <- peek ptr
        return
            ( coerce ((w .>>. 6) .|. r)
            , coerce (w .&. 0b00111111)
            , plusPtr ptr 1
            )
    | n == 2 = do
        return
            ( coerce (r .>>. 1)
            , coerce ((r .&. 0b11000001) .<<. 4)
            , ptr
            )
    | n == 3 = do
        w <- peek ptr
        return
            ( coerce ((w .>>. 4) .|. r)
            , coerce ((w .&. 0b00001111) .<<. 1)
            , plusPtr ptr 1
            )
    | n == 4 = do
        w <- peek ptr
        return
            ( coerce ((w .>>. 7) .|. r)
            , coerce (w .&. 0b01111111)
            , plusPtr ptr 1
            )
    | n == 5 = do
        return
            ( coerce (r .>>. 2)
            , coerce ((r .&. 0b10000011) .<<. 3)
            , ptr
            )
    | n == 6 = do
        w <- peek ptr
        return
            ( coerce ((w .>>. 5) .|. r)
            , coerce (w .&. 0b00011111)
            , plusPtr ptr 1
            )
    | otherwise = do
        return
            ( coerce r
            , coerce @Word8 0
            , ptr
            )

--
-- Bit Manipulation / Conversions
--

(.>>.) :: Bits a => a -> Int -> a
(.>>.) = unsafeShiftR
{-# SPECIALIZE INLINE (.>>.) :: Word8 -> Int -> Word8 #-}
{-# SPECIALIZE INLINE (.>>.) :: Word -> Int -> Word #-}

(.<<.) :: Bits a => a -> Int -> a
(.<<.) = unsafeShiftL
{-# SPECIALIZE INLINE (.<<.) :: Word8 -> Int -> Word8 #-}
{-# SPECIALIZE INLINE (.<<.) :: Word -> Int -> Word #-}

--
-- Dictionnary
--

-- | Fast array lookup of a word5 in an unboxed bytestring.
lookupWord5 :: Addr# -> Word5 -> Word8
lookupWord5 table (coerce -> (W8# i)) =
    W8# (indexWord8OffAddr# table (word2Int# i))
{-# INLINE lookupWord5 #-}