commit 16f82c42309d0b536771d961de44dcb775b7036a
parent 55f351f4c17608f6c384cddc4112418f1409674a
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 16 May 2026 21:17:58 -0230
Merge branch 'perf-refactor'
Low-level refactor of base32 + bech32 to drop the bytestring
'Builder' / 'toStrict' path and use direct buffer allocation
('BI.unsafeCreate' / 'BI.mallocByteString') with shared static
rodata tables, mirroring the recent ppad-base16 changes.
Module changes:
- Add 'Data.ByteString.Base32.Internal' (hidden) holding the two
rodata tables shared by 'Base32' and 'Bech32.Internal':
* 'enc_tab' (32 bytes): bech32 character set, 5-bit -> ASCII.
* 'dec_tab' (256 bytes): ASCII -> biased 5-bit (0x20..0x3f) or
a 0x40 sentinel for invalid chars. The bias keeps the literal
strictly ASCII with no embedded NUL, so the bytestring
'IsString' rule rewrites it to 'unsafePackAddress' and the
bytes live in static rodata. Validity is checked by OR-folding
every lookup and testing '.&. 0x40 == 0' once; the 5-bit value
is recovered with '.&. 0x1f'.
- 'Data.ByteString.Base32.encode' / 'decode' now precompute the
exact output length and fill it in-place; bit-shuffle and
canonical-form checks are unchanged in semantics.
- 'Data.ByteString.Bech32.Internal' rewrites 'as_word5',
'as_base32', 'hrp_expand', and 'create_checksum' in the same
idiom. 'as_word5' / 'as_base32' use the shared tables; the
duplicated 'bech32_charset' literal and 32-arm 'word5'
case-lambda are gone. 'create_checksum' writes the 6-byte
checksum directly into a 'BI.unsafeCreate' buffer rather than
'BS.map' over a shift-amount literal. 'polymod', 'verify',
'valid_hrp', and 'Encoding' are unchanged.
- 'Data.ByteString.Bech32' / 'Bech32m': final 4-piece concat
replaced with 'BS.concat'; 'Builder' imports and local
'toStrict' helpers dropped.
Verification:
- 'cabal test -f+llvm': all 6 properties pass (1000 cases each).
- Public API unchanged.
Benchmarks (M4 Air, -fllvm):
base32 encode 120b 55.24 ns -> 19.56 ns (2.82x)
base32 encode 128b 50.13 ns -> 19.95 ns (2.51x)
base32 encode 240b 79.78 ns -> 24.42 ns (3.27x)
base32 decode 120b 70.37 ns -> 24.19 ns (2.91x)
base32 decode 128b 66.59 ns -> 24.93 ns (2.67x)
bech32 encode 120b 456.4 ns -> 235.4 ns (1.94x)
bech32 decode 120b 508.8 ns -> 264.5 ns (1.92x)
Diffstat:
6 files changed, 409 insertions(+), 390 deletions(-)
diff --git a/lib/Data/ByteString/Base32.hs b/lib/Data/ByteString/Base32.hs
@@ -1,10 +1,5 @@
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE BinaryLiterals #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE MultiWayIf #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ViewPatterns #-}
-- |
-- Module: Data.ByteString.Base32
@@ -14,112 +9,27 @@
--
-- Unpadded base32 encoding & decoding using the bech32 character set.
--- this module is an adaptation of emilypi's 'base32' library
-
module Data.ByteString.Base32 (
-- * base32 encoding and decoding
encode
, decode
) where
-import Control.Monad (guard)
-import Data.Bits ((.|.), (.&.))
import qualified Data.Bits as B
+import Data.Bits ((.&.), (.|.))
import qualified Data.ByteString as BS
-import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Builder.Extra as BE
+import Data.ByteString.Base32.Internal (enc_tab, dec_tab)
import qualified Data.ByteString.Internal as BI
-import qualified Data.ByteString.Unsafe as BU
-import Data.Word (Word8, Word32, Word64)
+import Data.Word (Word8)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Ptr (Ptr, plusPtr)
+import Foreign.Storable (peekElemOff, pokeElemOff)
+import System.IO.Unsafe (unsafeDupablePerformIO)
-fi :: (Integral a, Num b) => a -> b
+fi :: (Num a, Integral b) => b -> a
fi = fromIntegral
{-# INLINE fi #-}
-word32be :: BS.ByteString -> Word32
-word32be s =
- (fi (s `BU.unsafeIndex` 0) `B.shiftL` 24) .|.
- (fi (s `BU.unsafeIndex` 1) `B.shiftL` 16) .|.
- (fi (s `BU.unsafeIndex` 2) `B.shiftL` 8) .|.
- (fi (s `BU.unsafeIndex` 3))
-{-# INLINE word32be #-}
-
--- realization for small builders
-toStrict :: BSB.Builder -> BS.ByteString
-toStrict = BS.toStrict
- . BE.toLazyByteStringWith (BE.safeStrategy 128 BE.smallChunkSize) mempty
-{-# INLINE toStrict #-}
-
-bech32_charset :: BS.ByteString
-bech32_charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
-
-word5 :: Word8 -> Maybe Word8
-word5 = \case
- 113 -> pure $! 0
- 112 -> pure $! 1
- 122 -> pure $! 2
- 114 -> pure $! 3
- 121 -> pure $! 4
- 57 -> pure $! 5
- 120 -> pure $! 6
- 56 -> pure $! 7
- 103 -> pure $! 8
- 102 -> pure $! 9
- 50 -> pure $! 10
- 116 -> pure $! 11
- 118 -> pure $! 12
- 100 -> pure $! 13
- 119 -> pure $! 14
- 48 -> pure $! 15
- 115 -> pure $! 16
- 51 -> pure $! 17
- 106 -> pure $! 18
- 110 -> pure $! 19
- 53 -> pure $! 20
- 52 -> pure $! 21
- 107 -> pure $! 22
- 104 -> pure $! 23
- 99 -> pure $! 24
- 101 -> pure $! 25
- 54 -> pure $! 26
- 109 -> pure $! 27
- 117 -> pure $! 28
- 97 -> pure $! 29
- 55 -> pure $! 30
- 108 -> pure $! 31
- _ -> Nothing
-{-# INLINE word5 #-}
-
-arrange :: Word32 -> Word8 -> BSB.Builder
-arrange w32 w8 =
- let mask = 0b00011111 -- low 5-bit mask
- bech32_char = fi . BS.index bech32_charset . fi -- word5 -> bech32
-
- -- split 40 bits into 8 w5's
- w5_0 = mask .&. (w32 `B.shiftR` 27) -- highest 5 bits
- w5_1 = mask .&. (w32 `B.shiftR` 22)
- w5_2 = mask .&. (w32 `B.shiftR` 17)
- w5_3 = mask .&. (w32 `B.shiftR` 12)
- w5_4 = mask .&. (w32 `B.shiftR` 07)
- w5_5 = mask .&. (w32 `B.shiftR` 02)
- -- combine lowest 2 bits of w32 with highest 3 bits of w8
- w5_6 = mask .&. (w32 `B.shiftL` 03 .|. fi w8 `B.shiftR` 05)
- -- lowest 5 bits of w8
- w5_7 = mask .&. fi w8
-
- -- get (w8) bech32 char for each w5, pack all into little-endian w64
- !w64 = bech32_char w5_0
- .|. bech32_char w5_1 `B.shiftL` 8
- .|. bech32_char w5_2 `B.shiftL` 16
- .|. bech32_char w5_3 `B.shiftL` 24
- .|. bech32_char w5_4 `B.shiftL` 32
- .|. bech32_char w5_5 `B.shiftL` 40
- .|. bech32_char w5_6 `B.shiftL` 48
- .|. bech32_char w5_7 `B.shiftL` 56
-
- in BSB.word64LE w64
-{-# INLINE arrange #-}
-
-- | Encode a base256-encoded 'ByteString' as a base32-encoded
-- 'ByteString', using the bech32 character set.
--
@@ -128,91 +38,101 @@ arrange w32 w8 =
encode
:: BS.ByteString -- ^ base256-encoded bytestring
-> BS.ByteString -- ^ base32-encoded bytestring
-encode dat = toStrict (go dat) where
- bech32_char = fi . BS.index bech32_charset . fi
-
- go bs@(BI.PS _ _ l)
- | l >= 5 = case BS.splitAt 5 bs of
- (chunk, etc) -> case BS.unsnoc chunk of
- Nothing -> error "impossible, chunk length is 5"
- Just (word32be -> w32, w8) -> arrange w32 w8 <> go etc
- | l == 0 = mempty
- | l == 1 =
- let a = BU.unsafeIndex bs 0
- t = bech32_char ((a .&. 0b11111000) `B.shiftR` 3)
- u = bech32_char ((a .&. 0b00000111) `B.shiftL` 2)
-
- !w16 = fi t
- .|. fi u `B.shiftL` 8
-
- in BSB.word16LE w16
- | l == 2 =
- let a = BU.unsafeIndex bs 0
- b = BU.unsafeIndex bs 1
- t = bech32_char ((a .&. 0b11111000) `B.shiftR` 3)
- u = bech32_char $
- ((a .&. 0b00000111) `B.shiftL` 2)
- .|. ((b .&. 0b11000000) `B.shiftR` 6)
- v = bech32_char ((b .&. 0b00111110) `B.shiftR` 1)
- w = bech32_char ((b .&. 0b00000001) `B.shiftL` 4)
-
- !w32 = fi t
- .|. fi u `B.shiftL` 8
- .|. fi v `B.shiftL` 16
- .|. fi w `B.shiftL` 24
-
- in BSB.word32LE w32
- | l == 3 =
- let a = BU.unsafeIndex bs 0
- b = BU.unsafeIndex bs 1
- c = BU.unsafeIndex bs 2
- t = bech32_char ((a .&. 0b11111000) `B.shiftR` 3)
- u = bech32_char $
- ((a .&. 0b00000111) `B.shiftL` 2)
- .|. ((b .&. 0b11000000) `B.shiftR` 6)
- v = bech32_char ((b .&. 0b00111110) `B.shiftR` 1)
- w = bech32_char $
- ((b .&. 0b00000001) `B.shiftL` 4)
- .|. ((c .&. 0b11110000) `B.shiftR` 4)
- x = bech32_char ((c .&. 0b00001111) `B.shiftL` 1)
-
- !w32 = fi t
- .|. fi u `B.shiftL` 8
- .|. fi v `B.shiftL` 16
- .|. fi w `B.shiftL` 24
-
- in BSB.word32LE w32 <> BSB.word8 x
- | l == 4 =
- let a = BU.unsafeIndex bs 0
- b = BU.unsafeIndex bs 1
- c = BU.unsafeIndex bs 2
- d = BU.unsafeIndex bs 3
- t = bech32_char ((a .&. 0b11111000) `B.shiftR` 3)
- u = bech32_char $
- ((a .&. 0b00000111) `B.shiftL` 2)
- .|. ((b .&. 0b11000000) `B.shiftR` 6)
- v = bech32_char ((b .&. 0b00111110) `B.shiftR` 1)
- w = bech32_char $
- ((b .&. 0b00000001) `B.shiftL` 4)
- .|. ((c .&. 0b11110000) `B.shiftR` 4)
- x = bech32_char $
- ((c .&. 0b00001111) `B.shiftL` 1)
- .|. ((d .&. 0b10000000) `B.shiftR` 7)
- y = bech32_char ((d .&. 0b01111100) `B.shiftR` 2)
- z = bech32_char ((d .&. 0b00000011) `B.shiftL` 3)
-
- !w32 = fi t
- .|. fi u `B.shiftL` 8
- .|. fi v `B.shiftL` 16
- .|. fi w `B.shiftL` 24
-
- !w16 = fi x
- .|. fi y `B.shiftL` 8
-
- in BSB.word32LE w32 <> BSB.word16LE w16 <> BSB.word8 z
-
- | otherwise =
- error "impossible"
+encode (BI.PS sfp soff l) = case enc_tab of
+ BI.PS tfp toff _ ->
+ let !outlen = (l * 8 + 4) `quot` 5
+ in BI.unsafeCreate outlen $ \dst ->
+ withForeignPtr sfp $ \sp0 ->
+ withForeignPtr tfp $ \tp0 -> do
+ let !sp = sp0 `plusPtr` soff :: Ptr Word8
+ !tp = tp0 `plusPtr` toff :: Ptr Word8
+ encode_loop sp tp dst l 0 0
+
+encode_loop
+ :: Ptr Word8 -> Ptr Word8 -> Ptr Word8
+ -> Int -> Int -> Int -> IO ()
+encode_loop !sp !tp !dst !len !i !j
+ | i + 5 <= len = do
+ a <- peekElemOff sp i
+ b <- peekElemOff sp (i + 1)
+ c <- peekElemOff sp (i + 2)
+ d <- peekElemOff sp (i + 3)
+ e <- peekElemOff sp (i + 4)
+ let !w0 = (a `B.shiftR` 3) .&. 0x1f
+ !w1 = (a `B.shiftL` 2 .|. b `B.shiftR` 6) .&. 0x1f
+ !w2 = (b `B.shiftR` 1) .&. 0x1f
+ !w3 = (b `B.shiftL` 4 .|. c `B.shiftR` 4) .&. 0x1f
+ !w4 = (c `B.shiftL` 1 .|. d `B.shiftR` 7) .&. 0x1f
+ !w5 = (d `B.shiftR` 2) .&. 0x1f
+ !w6 = (d `B.shiftL` 3 .|. e `B.shiftR` 5) .&. 0x1f
+ !w7 = e .&. 0x1f
+ peekElemOff tp (fi w0) >>= pokeElemOff dst j
+ peekElemOff tp (fi w1) >>= pokeElemOff dst (j + 1)
+ peekElemOff tp (fi w2) >>= pokeElemOff dst (j + 2)
+ peekElemOff tp (fi w3) >>= pokeElemOff dst (j + 3)
+ peekElemOff tp (fi w4) >>= pokeElemOff dst (j + 4)
+ peekElemOff tp (fi w5) >>= pokeElemOff dst (j + 5)
+ peekElemOff tp (fi w6) >>= pokeElemOff dst (j + 6)
+ peekElemOff tp (fi w7) >>= pokeElemOff dst (j + 7)
+ encode_loop sp tp dst len (i + 5) (j + 8)
+ | otherwise = encode_tail sp tp dst len i j
+
+encode_tail
+ :: Ptr Word8 -> Ptr Word8 -> Ptr Word8
+ -> Int -> Int -> Int -> IO ()
+encode_tail !sp !tp !dst !len !i !j = case len - i of
+ 0 -> pure ()
+ 1 -> do
+ a <- peekElemOff sp i
+ let !w0 = (a `B.shiftR` 3) .&. 0x1f
+ !w1 = (a `B.shiftL` 2) .&. 0x1f
+ peekElemOff tp (fi w0) >>= pokeElemOff dst j
+ peekElemOff tp (fi w1) >>= pokeElemOff dst (j + 1)
+ 2 -> do
+ a <- peekElemOff sp i
+ b <- peekElemOff sp (i + 1)
+ let !w0 = (a `B.shiftR` 3) .&. 0x1f
+ !w1 = (a `B.shiftL` 2 .|. b `B.shiftR` 6) .&. 0x1f
+ !w2 = (b `B.shiftR` 1) .&. 0x1f
+ !w3 = (b `B.shiftL` 4) .&. 0x1f
+ peekElemOff tp (fi w0) >>= pokeElemOff dst j
+ peekElemOff tp (fi w1) >>= pokeElemOff dst (j + 1)
+ peekElemOff tp (fi w2) >>= pokeElemOff dst (j + 2)
+ peekElemOff tp (fi w3) >>= pokeElemOff dst (j + 3)
+ 3 -> do
+ a <- peekElemOff sp i
+ b <- peekElemOff sp (i + 1)
+ c <- peekElemOff sp (i + 2)
+ let !w0 = (a `B.shiftR` 3) .&. 0x1f
+ !w1 = (a `B.shiftL` 2 .|. b `B.shiftR` 6) .&. 0x1f
+ !w2 = (b `B.shiftR` 1) .&. 0x1f
+ !w3 = (b `B.shiftL` 4 .|. c `B.shiftR` 4) .&. 0x1f
+ !w4 = (c `B.shiftL` 1) .&. 0x1f
+ peekElemOff tp (fi w0) >>= pokeElemOff dst j
+ peekElemOff tp (fi w1) >>= pokeElemOff dst (j + 1)
+ peekElemOff tp (fi w2) >>= pokeElemOff dst (j + 2)
+ peekElemOff tp (fi w3) >>= pokeElemOff dst (j + 3)
+ peekElemOff tp (fi w4) >>= pokeElemOff dst (j + 4)
+ 4 -> do
+ a <- peekElemOff sp i
+ b <- peekElemOff sp (i + 1)
+ c <- peekElemOff sp (i + 2)
+ d <- peekElemOff sp (i + 3)
+ let !w0 = (a `B.shiftR` 3) .&. 0x1f
+ !w1 = (a `B.shiftL` 2 .|. b `B.shiftR` 6) .&. 0x1f
+ !w2 = (b `B.shiftR` 1) .&. 0x1f
+ !w3 = (b `B.shiftL` 4 .|. c `B.shiftR` 4) .&. 0x1f
+ !w4 = (c `B.shiftL` 1 .|. d `B.shiftR` 7) .&. 0x1f
+ !w5 = (d `B.shiftR` 2) .&. 0x1f
+ !w6 = (d `B.shiftL` 3) .&. 0x1f
+ peekElemOff tp (fi w0) >>= pokeElemOff dst j
+ peekElemOff tp (fi w1) >>= pokeElemOff dst (j + 1)
+ peekElemOff tp (fi w2) >>= pokeElemOff dst (j + 2)
+ peekElemOff tp (fi w3) >>= pokeElemOff dst (j + 3)
+ peekElemOff tp (fi w4) >>= pokeElemOff dst (j + 4)
+ peekElemOff tp (fi w5) >>= pokeElemOff dst (j + 5)
+ peekElemOff tp (fi w6) >>= pokeElemOff dst (j + 6)
+ _ -> pure () -- impossible: 0 <= len - i < 5
-- | Decode a 'ByteString', encoded as base32 using the bech32 character
-- set, to a base256-encoded 'ByteString'.
@@ -224,108 +144,166 @@ encode dat = toStrict (go dat) where
decode
:: BS.ByteString -- ^ base32-encoded bytestring
-> Maybe BS.ByteString -- ^ base256-encoded bytestring
-decode = fmap toStrict . go mempty where
- go acc bs@(BI.PS _ _ l)
- | l < 8 = do
- fin <- finalize bs
- pure (acc <> fin)
- | otherwise = case BS.splitAt 8 bs of
- (chunk, etc) -> do
- res <- decode_chunk chunk
- go (acc <> res) etc
-
-finalize :: BS.ByteString -> Maybe BSB.Builder
-finalize bs@(BI.PS _ _ l)
- | l == 0 = Just mempty
- | otherwise = do
- guard (l >= 2)
- w5_0 <- word5 (BU.unsafeIndex bs 0)
- w5_1 <- word5 (BU.unsafeIndex bs 1)
- let w8_0 = w5_0 `B.shiftL` 3
- .|. w5_1 `B.shiftR` 2
-
- -- https://datatracker.ietf.org/doc/html/rfc4648#section-6
- if | l == 2 -> do -- 2 w5's, need 1 w8; 2 bits remain
- guard (w5_1 `B.shiftL` 6 == 0)
- pure (BSB.word8 w8_0)
-
- | l == 4 -> do -- 4 w5's, need 2 w8's; 4 bits remain
- w5_2 <- word5 (BU.unsafeIndex bs 2)
- w5_3 <- word5 (BU.unsafeIndex bs 3)
- let w8_1 = w5_1 `B.shiftL` 6
- .|. w5_2 `B.shiftL` 1
- .|. w5_3 `B.shiftR` 4
-
- !w16 = fi w8_1
- .|. fi w8_0 `B.shiftL` 8
-
- guard (w5_3 `B.shiftL` 4 == 0)
- pure (BSB.word16BE w16)
-
- | l == 5 -> do -- 5 w5's, need 3 w8's; 1 bit remains
- w5_2 <- word5 (BU.unsafeIndex bs 2)
- w5_3 <- word5 (BU.unsafeIndex bs 3)
- w5_4 <- word5 (BU.unsafeIndex bs 4)
- let w8_1 = w5_1 `B.shiftL` 6
- .|. w5_2 `B.shiftL` 1
- .|. w5_3 `B.shiftR` 4
- w8_2 = w5_3 `B.shiftL` 4
- .|. w5_4 `B.shiftR` 1
-
- w16 = fi w8_1
- .|. fi w8_0 `B.shiftL` 8
-
- guard (w5_4 `B.shiftL` 7 == 0)
- pure (BSB.word16BE w16 <> BSB.word8 w8_2)
-
- | l == 7 -> do -- 7 w5's, need 4 w8's; 3 bits remain
- w5_2 <- word5 (BU.unsafeIndex bs 2)
- w5_3 <- word5 (BU.unsafeIndex bs 3)
- w5_4 <- word5 (BU.unsafeIndex bs 4)
- w5_5 <- word5 (BU.unsafeIndex bs 5)
- w5_6 <- word5 (BU.unsafeIndex bs 6)
- let w8_1 = w5_1 `B.shiftL` 6
- .|. w5_2 `B.shiftL` 1
- .|. w5_3 `B.shiftR` 4
- w8_2 = w5_3 `B.shiftL` 4
- .|. w5_4 `B.shiftR` 1
- w8_3 = w5_4 `B.shiftL` 7
- .|. w5_5 `B.shiftL` 2
- .|. w5_6 `B.shiftR` 3
-
- w32 = fi w8_3
- .|. fi w8_2 `B.shiftL` 8
- .|. fi w8_1 `B.shiftL` 16
- .|. fi w8_0 `B.shiftL` 24
-
- guard (w5_6 `B.shiftL` 5 == 0)
- pure (BSB.word32BE w32)
-
- | otherwise -> Nothing
-
--- assumes length 8 input
-decode_chunk :: BS.ByteString -> Maybe BSB.Builder
-decode_chunk bs = do
- w5_0 <- word5 (BU.unsafeIndex bs 0)
- w5_1 <- word5 (BU.unsafeIndex bs 1)
- w5_2 <- word5 (BU.unsafeIndex bs 2)
- w5_3 <- word5 (BU.unsafeIndex bs 3)
- w5_4 <- word5 (BU.unsafeIndex bs 4)
- w5_5 <- word5 (BU.unsafeIndex bs 5)
- w5_6 <- word5 (BU.unsafeIndex bs 6)
- w5_7 <- word5 (BU.unsafeIndex bs 7)
-
- let w40 :: Word64
- !w40 = fi w5_0 `B.shiftL` 35
- .|. fi w5_1 `B.shiftL` 30
- .|. fi w5_2 `B.shiftL` 25
- .|. fi w5_3 `B.shiftL` 20
- .|. fi w5_4 `B.shiftL` 15
- .|. fi w5_5 `B.shiftL` 10
- .|. fi w5_6 `B.shiftL` 05
- .|. fi w5_7
- !w32 = fi (w40 `B.shiftR` 8) :: Word32
- !w8 = fi (0b11111111 .&. w40) :: Word8
-
- pure $ BSB.word32BE w32 <> BSB.word8 w8
-
+decode (BI.PS sfp soff l) = case l `rem` 8 of
+ 1 -> Nothing
+ 3 -> Nothing
+ 6 -> Nothing
+ _ -> case dec_tab of
+ BI.PS tfp toff _ -> unsafeDupablePerformIO $ do
+ let !n = (l * 5) `B.shiftR` 3
+ fp <- BI.mallocByteString n
+ ok <- withForeignPtr fp $ \dst ->
+ withForeignPtr sfp $ \sp0 ->
+ withForeignPtr tfp $ \tp0 -> do
+ let !sp = sp0 `plusPtr` soff :: Ptr Word8
+ !tp = tp0 `plusPtr` toff :: Ptr Word8
+ decode_loop sp tp dst l 0 0 0
+ pure $! if ok then Just (BI.PS fp 0 n) else Nothing
+
+decode_loop
+ :: Ptr Word8 -> Ptr Word8 -> Ptr Word8
+ -> Int -> Int -> Int -> Word8 -> IO Bool
+decode_loop !sp !tp !dst !len !i !j !acc
+ | i + 8 <= len = do
+ c0 <- peekElemOff sp i
+ c1 <- peekElemOff sp (i + 1)
+ c2 <- peekElemOff sp (i + 2)
+ c3 <- peekElemOff sp (i + 3)
+ c4 <- peekElemOff sp (i + 4)
+ c5 <- peekElemOff sp (i + 5)
+ c6 <- peekElemOff sp (i + 6)
+ c7 <- peekElemOff sp (i + 7)
+ n0 <- peekElemOff tp (fi c0)
+ n1 <- peekElemOff tp (fi c1)
+ n2 <- peekElemOff tp (fi c2)
+ n3 <- peekElemOff tp (fi c3)
+ n4 <- peekElemOff tp (fi c4)
+ n5 <- peekElemOff tp (fi c5)
+ n6 <- peekElemOff tp (fi c6)
+ n7 <- peekElemOff tp (fi c7)
+ let !v0 = n0 .&. 0x1f
+ !v1 = n1 .&. 0x1f
+ !v2 = n2 .&. 0x1f
+ !v3 = n3 .&. 0x1f
+ !v4 = n4 .&. 0x1f
+ !v5 = n5 .&. 0x1f
+ !v6 = n6 .&. 0x1f
+ !v7 = n7 .&. 0x1f
+ !b0 = (v0 `B.shiftL` 3) .|. (v1 `B.shiftR` 2)
+ !b1 = (v1 `B.shiftL` 6) .|. (v2 `B.shiftL` 1) .|.
+ (v3 `B.shiftR` 4)
+ !b2 = (v3 `B.shiftL` 4) .|. (v4 `B.shiftR` 1)
+ !b3 = (v4 `B.shiftL` 7) .|. (v5 `B.shiftL` 2) .|.
+ (v6 `B.shiftR` 3)
+ !b4 = (v6 `B.shiftL` 5) .|. v7
+ pokeElemOff dst j b0
+ pokeElemOff dst (j + 1) b1
+ pokeElemOff dst (j + 2) b2
+ pokeElemOff dst (j + 3) b3
+ pokeElemOff dst (j + 4) b4
+ decode_loop sp tp dst len (i + 8) (j + 5)
+ (acc .|. n0 .|. n1 .|. n2 .|. n3 .|. n4 .|. n5 .|. n6 .|. n7)
+ | otherwise = decode_tail sp tp dst len i j acc
+
+decode_tail
+ :: Ptr Word8 -> Ptr Word8 -> Ptr Word8
+ -> Int -> Int -> Int -> Word8 -> IO Bool
+decode_tail !sp !tp !dst !len !i !j !acc = case len - i of
+ 0 -> pure $! acc .&. 0x40 == 0
+ 2 -> do
+ c0 <- peekElemOff sp i
+ c1 <- peekElemOff sp (i + 1)
+ n0 <- peekElemOff tp (fi c0)
+ n1 <- peekElemOff tp (fi c1)
+ let !v0 = n0 .&. 0x1f
+ !v1 = n1 .&. 0x1f
+ !b0 = (v0 `B.shiftL` 3) .|. (v1 `B.shiftR` 2)
+ -- canonical-form check: bits dropped from v1 must be zero
+ !slack = v1 `B.shiftL` 6
+ pokeElemOff dst j b0
+ pure $! (acc .|. n0 .|. n1) .&. 0x40 == 0 && slack == 0
+ 4 -> do
+ c0 <- peekElemOff sp i
+ c1 <- peekElemOff sp (i + 1)
+ c2 <- peekElemOff sp (i + 2)
+ c3 <- peekElemOff sp (i + 3)
+ n0 <- peekElemOff tp (fi c0)
+ n1 <- peekElemOff tp (fi c1)
+ n2 <- peekElemOff tp (fi c2)
+ n3 <- peekElemOff tp (fi c3)
+ let !v0 = n0 .&. 0x1f
+ !v1 = n1 .&. 0x1f
+ !v2 = n2 .&. 0x1f
+ !v3 = n3 .&. 0x1f
+ !b0 = (v0 `B.shiftL` 3) .|. (v1 `B.shiftR` 2)
+ !b1 = (v1 `B.shiftL` 6) .|. (v2 `B.shiftL` 1) .|.
+ (v3 `B.shiftR` 4)
+ !slack = v3 `B.shiftL` 4
+ pokeElemOff dst j b0
+ pokeElemOff dst (j + 1) b1
+ pure $! (acc .|. n0 .|. n1 .|. n2 .|. n3) .&. 0x40 == 0
+ && slack == 0
+ 5 -> do
+ c0 <- peekElemOff sp i
+ c1 <- peekElemOff sp (i + 1)
+ c2 <- peekElemOff sp (i + 2)
+ c3 <- peekElemOff sp (i + 3)
+ c4 <- peekElemOff sp (i + 4)
+ n0 <- peekElemOff tp (fi c0)
+ n1 <- peekElemOff tp (fi c1)
+ n2 <- peekElemOff tp (fi c2)
+ n3 <- peekElemOff tp (fi c3)
+ n4 <- peekElemOff tp (fi c4)
+ let !v0 = n0 .&. 0x1f
+ !v1 = n1 .&. 0x1f
+ !v2 = n2 .&. 0x1f
+ !v3 = n3 .&. 0x1f
+ !v4 = n4 .&. 0x1f
+ !b0 = (v0 `B.shiftL` 3) .|. (v1 `B.shiftR` 2)
+ !b1 = (v1 `B.shiftL` 6) .|. (v2 `B.shiftL` 1) .|.
+ (v3 `B.shiftR` 4)
+ !b2 = (v3 `B.shiftL` 4) .|. (v4 `B.shiftR` 1)
+ !slack = v4 `B.shiftL` 7
+ pokeElemOff dst j b0
+ pokeElemOff dst (j + 1) b1
+ pokeElemOff dst (j + 2) b2
+ pure $! (acc .|. n0 .|. n1 .|. n2 .|. n3 .|. n4) .&. 0x40 == 0
+ && slack == 0
+ 7 -> do
+ c0 <- peekElemOff sp i
+ c1 <- peekElemOff sp (i + 1)
+ c2 <- peekElemOff sp (i + 2)
+ c3 <- peekElemOff sp (i + 3)
+ c4 <- peekElemOff sp (i + 4)
+ c5 <- peekElemOff sp (i + 5)
+ c6 <- peekElemOff sp (i + 6)
+ n0 <- peekElemOff tp (fi c0)
+ n1 <- peekElemOff tp (fi c1)
+ n2 <- peekElemOff tp (fi c2)
+ n3 <- peekElemOff tp (fi c3)
+ n4 <- peekElemOff tp (fi c4)
+ n5 <- peekElemOff tp (fi c5)
+ n6 <- peekElemOff tp (fi c6)
+ let !v0 = n0 .&. 0x1f
+ !v1 = n1 .&. 0x1f
+ !v2 = n2 .&. 0x1f
+ !v3 = n3 .&. 0x1f
+ !v4 = n4 .&. 0x1f
+ !v5 = n5 .&. 0x1f
+ !v6 = n6 .&. 0x1f
+ !b0 = (v0 `B.shiftL` 3) .|. (v1 `B.shiftR` 2)
+ !b1 = (v1 `B.shiftL` 6) .|. (v2 `B.shiftL` 1) .|.
+ (v3 `B.shiftR` 4)
+ !b2 = (v3 `B.shiftL` 4) .|. (v4 `B.shiftR` 1)
+ !b3 = (v4 `B.shiftL` 7) .|. (v5 `B.shiftL` 2) .|.
+ (v6 `B.shiftR` 3)
+ !slack = v6 `B.shiftL` 5
+ pokeElemOff dst j b0
+ pokeElemOff dst (j + 1) b1
+ pokeElemOff dst (j + 2) b2
+ pokeElemOff dst (j + 3) b3
+ pure $!
+ (acc .|. n0 .|. n1 .|. n2 .|. n3 .|. n4 .|. n5 .|. n6)
+ .&. 0x40 == 0
+ && slack == 0
+ _ -> pure False -- impossible: tail-length guard already rejected
diff --git a/lib/Data/ByteString/Base32/Internal.hs b/lib/Data/ByteString/Base32/Internal.hs
@@ -0,0 +1,59 @@
+{-# OPTIONS_HADDOCK hide, prune #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Module: Data.ByteString.Base32.Internal
+-- Copyright: (c) 2024 Jared Tobin
+-- License: MIT
+-- Maintainer: Jared Tobin <jared@ppad.tech>
+--
+-- Static rodata tables for the bech32 base32 charset, shared by
+-- 'Data.ByteString.Base32' and 'Data.ByteString.Bech32.Internal'.
+
+module Data.ByteString.Base32.Internal (
+ enc_tab
+ , dec_tab
+ ) where
+
+import qualified Data.ByteString as BS
+
+-- 32-byte encoding table: the bech32 character set. Maps a 5-bit
+-- value (0..31) to its bech32 character. ASCII-only with no embedded
+-- NUL, so the bytestring 'IsString' rule rewrites the literal to
+-- 'unsafePackAddress' and the bytes live in static rodata.
+enc_tab :: BS.ByteString
+enc_tab = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+{-# NOINLINE enc_tab #-}
+
+-- 256-byte reverse table. Index by an ASCII byte to obtain its
+-- 5-bit value (biased into bit 5); valid bech32 chars map to
+-- 0x20..0x3f, every other byte maps to 0x40.
+--
+-- The encoding is chosen so the literal is strictly ASCII and
+-- contains no embedded NUL, which is what the bytestring 'IsString'
+-- rule needs to rewrite it into 'unsafePackAddress' (cf. 'enc_tab')
+-- - the bytes end up in static rodata, with no CAF allocation.
+--
+-- The 0x40 sentinel is distinguished by bit 6; no value 0x20..0x3f
+-- carries that bit, so callers OR-fold every lookup into an
+-- accumulator and test 'acc .&. 0x40 == 0' once at the end. The
+-- 5-bit value is extracted as 'b .&. 0x1f'.
+dec_tab :: BS.ByteString
+dec_tab =
+ "\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x2f\x40\x2a\x31\x35\x34\x3a\x3e\x27\x25\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x3d\x40\x38\x2d\x39\x29\x28\x37\x40\x32\x36\x3f\x3b\x33\x40\
+ \\x21\x20\x23\x30\x2b\x3c\x2c\x2e\x26\x24\x22\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\
+ \\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40\x40"
+{-# NOINLINE dec_tab #-}
diff --git a/lib/Data/ByteString/Bech32.hs b/lib/Data/ByteString/Bech32.hs
@@ -25,17 +25,9 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Base32 as B32
import qualified Data.ByteString.Bech32.Internal as BI
-import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Builder.Extra as BE
import qualified Data.ByteString.Internal as BSI
import qualified Data.Char as C (toLower, isLower, isAlpha)
--- realization for small builders
-toStrict :: BSB.Builder -> BS.ByteString
-toStrict = BS.toStrict
- . BE.toLazyByteStringWith (BE.safeStrategy 128 BE.smallChunkSize) mempty
-{-# INLINE toStrict #-}
-
create_checksum :: BS.ByteString -> BS.ByteString -> BS.ByteString
create_checksum = BI.create_checksum BI.Bech32
@@ -52,11 +44,7 @@ encode (B8.map C.toLower -> hrp) (B32.encode -> dat) = do
guard (BI.valid_hrp hrp)
ws <- BI.as_word5 dat
let check = create_checksum hrp ws
- res = toStrict $
- BSB.byteString hrp
- <> BSB.word8 49 -- 1
- <> BSB.byteString dat
- <> BSB.byteString (BI.as_base32 check)
+ res = BS.concat [hrp, BS.singleton 49, dat, BI.as_base32 check]
guard (BS.length res < 91)
pure res
diff --git a/lib/Data/ByteString/Bech32/Internal.hs b/lib/Data/ByteString/Bech32/Internal.hs
@@ -1,8 +1,6 @@
{-# OPTIONS_HADDOCK hide, prune #-}
{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module Data.ByteString.Bech32.Internal (
@@ -14,80 +12,65 @@ module Data.ByteString.Bech32.Internal (
, valid_hrp
) where
-import Data.Bits ((.&.))
+import Data.Bits ((.&.), (.|.))
import qualified Data.Bits as B
import qualified Data.ByteString as BS
-import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Builder.Extra as BE
+import Data.ByteString.Base32.Internal (enc_tab, dec_tab)
import qualified Data.ByteString.Internal as BI
import qualified Data.ByteString.Unsafe as BU
import Data.Word (Word8, Word32)
+import Foreign.ForeignPtr (withForeignPtr)
+import Foreign.Ptr (Ptr, plusPtr)
+import Foreign.Storable (peekElemOff, pokeElemOff)
+import System.IO.Unsafe (unsafeDupablePerformIO)
fi :: (Integral a, Num b) => a -> b
fi = fromIntegral
{-# INLINE fi #-}
--- realization for small builders
-toStrict :: BSB.Builder -> BS.ByteString
-toStrict = BS.toStrict
- . BE.toLazyByteStringWith (BE.safeStrategy 128 BE.smallChunkSize) mempty
-{-# INLINE toStrict #-}
-
_BECH32M_CONST :: Word32
_BECH32M_CONST = 0x2bc830a3
-bech32_charset :: BS.ByteString
-bech32_charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
-
-word5 :: Word8 -> Maybe Word8
-word5 = \case
- 113 -> pure $! 00 -- 'q'
- 112 -> pure $! 01 -- 'p'
- 122 -> pure $! 02 -- 'z'
- 114 -> pure $! 03 -- 'r'
- 121 -> pure $! 04 -- 'y'
- 57 -> pure $! 05 -- '9'
- 120 -> pure $! 06 -- 'x'
- 56 -> pure $! 07 -- '8'
- 103 -> pure $! 08 -- 'g'
- 102 -> pure $! 09 -- 'f'
- 50 -> pure $! 10 -- '2'
- 116 -> pure $! 11 -- 't'
- 118 -> pure $! 12 -- 'v'
- 100 -> pure $! 13 -- 'd'
- 119 -> pure $! 14 -- 'w'
- 48 -> pure $! 15 -- '0'
- 115 -> pure $! 16 -- 's'
- 51 -> pure $! 17 -- '3'
- 106 -> pure $! 18 -- 'j'
- 110 -> pure $! 19 -- 'n'
- 53 -> pure $! 20 -- '5'
- 52 -> pure $! 21 -- '4'
- 107 -> pure $! 22 -- 'k'
- 104 -> pure $! 23 -- 'h'
- 99 -> pure $! 24 -- 'c'
- 101 -> pure $! 25 -- 'e'
- 54 -> pure $! 26 -- '6'
- 109 -> pure $! 27 -- 'm'
- 117 -> pure $! 28 -- 'u'
- 97 -> pure $! 29 -- 'a'
- 55 -> pure $! 30 -- '7'
- 108 -> pure $! 31 -- 'l'
- _ -> Nothing
-{-# INLINE word5 #-}
-
--- base32 -> word5
+-- | Translate base32 bytestring to its 5-bit-value bytestring. Each
+-- input byte is looked up in 'dec_tab'; if any byte is not a valid
+-- bech32 char, returns 'Nothing'.
as_word5 :: BS.ByteString -> Maybe BS.ByteString
-as_word5 = go mempty where
- go acc bs = case BS.uncons bs of
- Nothing -> pure (toStrict acc)
- Just (h, t) -> do
- w5 <- word5 (fi h)
- go (acc <> BSB.word8 w5) t
-
--- word5 -> base32
+as_word5 (BI.PS sfp soff l) = case dec_tab of
+ BI.PS tfp toff _ -> unsafeDupablePerformIO $ do
+ fp <- BI.mallocByteString l
+ ok <- withForeignPtr fp $ \dst ->
+ withForeignPtr sfp $ \sp0 ->
+ withForeignPtr tfp $ \tp0 -> do
+ let !sp = sp0 `plusPtr` soff :: Ptr Word8
+ !tp = tp0 `plusPtr` toff :: Ptr Word8
+ loop !i !acc
+ | i == l = pure $! acc .&. 0x40 == 0
+ | otherwise = do
+ c <- peekElemOff sp i
+ n <- peekElemOff tp (fi c)
+ pokeElemOff dst i (n .&. 0x1f)
+ loop (i + 1) (acc .|. n)
+ loop 0 0
+ pure $! if ok then Just (BI.PS fp 0 l) else Nothing
+
+-- | Translate a 5-bit-value bytestring to its bech32 base32
+-- bytestring.
as_base32 :: BS.ByteString -> BS.ByteString
-as_base32 = BS.map (BU.unsafeIndex bech32_charset . fi)
+as_base32 (BI.PS sfp soff l) = case enc_tab of
+ BI.PS tfp toff _ ->
+ BI.unsafeCreate l $ \dst ->
+ withForeignPtr sfp $ \sp0 ->
+ withForeignPtr tfp $ \tp0 -> do
+ let !sp = sp0 `plusPtr` soff :: Ptr Word8
+ !tp = tp0 `plusPtr` toff :: Ptr Word8
+ loop !i
+ | i == l = pure ()
+ | otherwise = do
+ v <- peekElemOff sp i
+ c <- peekElemOff tp (fi v)
+ pokeElemOff dst i c
+ loop (i + 1)
+ loop 0
polymod :: BS.ByteString -> Word32
polymod = BS.foldl' alg 1 where
@@ -117,29 +100,51 @@ valid_hrp hrp@(BI.PS _ _ l)
| l == 0 || l > 83 = False
| otherwise = BS.all (\b -> (b > 32) && (b < 127)) hrp
+-- | Build the bech32 HRP expansion: high-5-bits of each HRP byte,
+-- then a single 0, then low-5-bits of each HRP byte.
hrp_expand :: BS.ByteString -> BS.ByteString
-hrp_expand bs = toStrict
- $ BSB.byteString (BS.map (`B.shiftR` 5) bs)
- <> BSB.word8 0
- <> BSB.byteString (BS.map (.&. 0b11111) bs)
+hrp_expand (BI.PS sfp soff l) =
+ BI.unsafeCreate (2 * l + 1) $ \dst ->
+ withForeignPtr sfp $ \sp0 -> do
+ let !sp = sp0 `plusPtr` soff :: Ptr Word8
+ loop_hi !i
+ | i == l = pure ()
+ | otherwise = do
+ c <- peekElemOff sp i
+ pokeElemOff dst i (c `B.shiftR` 5)
+ loop_hi (i + 1)
+ loop_lo !i
+ | i == l = pure ()
+ | otherwise = do
+ c <- peekElemOff sp i
+ pokeElemOff dst (l + 1 + i) (c .&. 0x1f)
+ loop_lo (i + 1)
+ loop_hi 0
+ pokeElemOff dst l (0 :: Word8)
+ loop_lo 0
data Encoding =
Bech32
| Bech32m
-create_checksum :: Encoding -> BS.ByteString -> BS.ByteString -> BS.ByteString
+zero6 :: BS.ByteString
+zero6 = BS.replicate 6 0
+{-# NOINLINE zero6 #-}
+
+create_checksum
+ :: Encoding -> BS.ByteString -> BS.ByteString -> BS.ByteString
create_checksum enc hrp dat =
- let pre = hrp_expand hrp <> dat
- pay = toStrict $
- BSB.byteString pre
- <> BSB.byteString "\NUL\NUL\NUL\NUL\NUL\NUL"
- pm = polymod pay `B.xor` case enc of
+ let !pay = BS.concat [hrp_expand hrp, dat, zero6]
+ !pm = polymod pay `B.xor` case enc of
Bech32 -> 1
Bech32m -> _BECH32M_CONST
-
- code i = (fi (pm `B.shiftR` fi i) .&. 0b11111)
-
- in BS.map code "\EM\DC4\SI\n\ENQ\NUL" -- BS.pack [25, 20, 15, 10, 5, 0]
+ in BI.unsafeCreate 6 $ \dst -> do
+ pokeElemOff dst 0 (fi (pm `B.shiftR` 25) .&. 0x1f :: Word8)
+ pokeElemOff dst 1 (fi (pm `B.shiftR` 20) .&. 0x1f :: Word8)
+ pokeElemOff dst 2 (fi (pm `B.shiftR` 15) .&. 0x1f :: Word8)
+ pokeElemOff dst 3 (fi (pm `B.shiftR` 10) .&. 0x1f :: Word8)
+ pokeElemOff dst 4 (fi (pm `B.shiftR` 5) .&. 0x1f :: Word8)
+ pokeElemOff dst 5 (fi pm .&. 0x1f :: Word8)
verify :: Encoding -> BS.ByteString -> Bool
verify enc b32 = case BS.elemIndexEnd 0x31 b32 of
@@ -154,4 +159,3 @@ verify enc b32 = case BS.elemIndexEnd 0x31 b32 of
in polymod bs == case enc of
Bech32 -> 1
Bech32m -> _BECH32M_CONST
-
diff --git a/lib/Data/ByteString/Bech32m.hs b/lib/Data/ByteString/Bech32m.hs
@@ -26,17 +26,9 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Base32 as B32
import qualified Data.ByteString.Bech32.Internal as BI
-import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Builder.Extra as BE
import qualified Data.ByteString.Internal as BSI
import qualified Data.Char as C (toLower)
--- realization for small builders
-toStrict :: BSB.Builder -> BS.ByteString
-toStrict = BS.toStrict
- . BE.toLazyByteStringWith (BE.safeStrategy 128 BE.smallChunkSize) mempty
-{-# INLINE toStrict #-}
-
create_checksum :: BS.ByteString -> BS.ByteString -> BS.ByteString
create_checksum = BI.create_checksum BI.Bech32m
@@ -53,11 +45,7 @@ encode (B8.map C.toLower -> hrp) (B32.encode -> dat) = do
guard (BI.valid_hrp hrp)
ws <- BI.as_word5 dat
let check = create_checksum hrp ws
- res = toStrict $
- BSB.byteString hrp
- <> BSB.word8 49 -- 1
- <> BSB.byteString dat
- <> BSB.byteString (BI.as_base32 check)
+ res = BS.concat [hrp, BS.singleton 49, dat, BI.as_base32 check]
guard (BS.length res < 91)
pure res
diff --git a/ppad-bech32.cabal b/ppad-bech32.cabal
@@ -35,6 +35,8 @@ library
, Data.ByteString.Bech32.Internal
, Data.ByteString.Bech32
, Data.ByteString.Bech32m
+ other-modules:
+ Data.ByteString.Base32.Internal
build-depends:
base >= 4.9 && < 5
, bytestring >= 0.9 && < 0.13